Skip to main content
Version: 1.4.x

Context Propagation

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


Tracing Context Propagation

The namastack-outbox-tracing module provides automatic, zero-configuration trace propagation. No custom OutboxContextProvider bean is needed.

info

See the Observability guide for full details.

For observations, tracing, and the zero-configuration tracing module, see the Observability section.


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