Skip to main content
Version: Next

Context Propagation

Context propagation preserves important metadata (trace IDs, tenant info, correlation IDs, user context) across the async boundary between scheduling and processing.

How It Works

Context is captured when records are scheduled and restored when handlers are invoked:


OutboxContextProvider Interface

@Component
class TracingContextProvider(
private val tracer: Tracer
) : OutboxContextProvider {
override fun provide(): Map<String, String> {
val currentSpan = tracer.currentSpan() ?: return emptyMap()
return mapOf(
"traceId" to currentSpan.context().traceId(),
"spanId" to currentSpan.context().spanId()
)
}
}

@Component
class TenantContextProvider : OutboxContextProvider {
override fun provide(): Map<String, String> {
val tenantId = SecurityContextHolder.getContext()
.authentication
?.principal
?.let { (it as? TenantAware)?.tenantId }
?: return emptyMap()
return mapOf("tenantId" to tenantId)
}
}

Multiple Providers:

You can register multiple OutboxContextProvider beans. The library automatically merges all context maps. If keys collide, the last provider wins.


Accessing Context in Handlers

@Component
class OrderHandler {
@OutboxHandler
fun handle(payload: OrderEvent, metadata: OutboxRecordMetadata) {
// Access context via metadata.context
val traceId = metadata.context["traceId"]
val spanId = metadata.context["spanId"]
val tenantId = metadata.context["tenantId"]

// Restore tracing context for downstream calls
tracer.withSpan(traceId, spanId) {
logger.info("Processing order ${payload.orderId} [trace: $traceId]")
orderService.process(payload)
}
}

@OutboxFallbackHandler
fun handleFailure(payload: OrderEvent, context: OutboxFailureContext) {
// Access context via failureContext.context
val traceId = context.context["traceId"]

logger.error(
"Order ${payload.orderId} failed permanently [trace: $traceId]",
context.lastException
)

deadLetterQueue.publish(
payload = payload,
context = mapOf("traceId" to traceId)
)
}
}

Manual Context Override

@Service
class OrderService(private val outbox: Outbox) {
@Transactional
fun createOrder(command: CreateOrderCommand) {
val order = Order.create(command)
orderRepository.save(order)

// Override or extend context
outbox.schedule(
payload = OrderCreatedEvent(order.id, order.customerId),
key = "order-${order.id}",
context = mapOf(
"correlationId" to command.correlationId,
"userId" to command.userId,
"priority" to "high"
)
)
}
}

Context Merging:

When you provide manual context, it's merged with context from registered OutboxContextProvider beans. Manual context takes precedence for duplicate keys.


Use Cases

Common use cases for context propagation:

  1. Distributed Tracing: Preserve trace and span IDs across async boundaries
  2. Multi-Tenancy: Maintain tenant context for data isolation
  3. Correlation IDs: Track requests across service boundaries
  4. User Context: Preserve user identity for audit logging
  5. Feature Flags: Propagate feature flag states for consistent behavior
  6. Request Metadata: Pass request IDs, client info, API versions