Overview
Distributed Context Propagation enables consistent tracking and propagation of contextual information across service boundaries in distributed systems. This is essential for observability, tracing, and maintaining request-specific state in microservices architectures.
Core Concepts
1. Context Elements
- Trace Context: Distributed tracing identifiers
- Baggage: Custom key-value pairs for business context
- Correlation IDs: Request correlation across services
- Security Context: Authentication and authorization tokens
- Tenant Context: Multi-tenancy information
2. Propagation Mechanisms
- HTTP Headers: Most common for synchronous communication
- Message Properties: For asynchronous messaging (Kafka, RabbitMQ)
- gRPC Metadata: For gRPC-based communication
- Thread-Local Storage: For in-process context propagation
Implementation Architectures
1. Manual Context Propagation
public class ManualContextPropagator { private static final String TRACE_ID_HEADER = "X-Trace-Id"; private static final String SPAN_ID_HEADER = "X-Span-Id"; private static final String CORRELATION_ID_HEADER = "X-Correlation-Id"; private static final String TENANT_ID_HEADER = "X-Tenant-Id"; // Outgoing HTTP request context propagation public HttpHeaders propagateContextForOutgoingRequest() { DistributedContext context = DistributedContext.getCurrent(); HttpHeaders headers = new HttpHeaders(); headers.set(TRACE_ID_HEADER, context.getTraceId()); headers.set(SPAN_ID_HEADER, context.getSpanId()); headers.set(CORRELATION_ID_HEADER, context.getCorrelationId()); headers.set(TENANT_ID_HEADER, context.getTenantId()); // Propagate baggage context.getBaggage().forEach((key, value) -> { headers.set("X-Baggage-" + key, value); }); return headers; } // Incoming HTTP request context extraction public void extractContextFromIncomingRequest(HttpServletRequest request) { String traceId = getHeaderOrDefault(request, TRACE_ID_HEADER, generateTraceId()); String spanId = getHeaderOrDefault(request, SPAN_ID_HEADER, generateSpanId()); String correlationId = getHeaderOrDefault(request, CORRELATION_ID_HEADER, generateCorrelationId()); String tenantId = getHeaderOrDefault(request, TENANT_ID_HEADER, "default"); DistributedContext context = DistributedContext.create() .withTraceId(traceId) .withSpanId(spanId) .withCorrelationId(correlationId) .withTenantId(tenantId); // Extract baggage Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); if (headerName.startsWith("X-Baggage-")) { String key = headerName.substring("X-Baggage-".length()); String value = request.getHeader(headerName); context.withBaggageItem(key, value); } } DistributedContext.setCurrent(context); } private String getHeaderOrDefault(HttpServletRequest request, String header, String defaultValue) { String value = request.getHeader(header); return value != null ? value : defaultValue; } } 2. Automatic Context Propagation Framework
public class DistributedContext implements AutoCloseable { private static final ThreadLocal<DistributedContext> CURRENT_CONTEXT = new InheritableThreadLocal<>(); private final String traceId; private final String spanId; private final String parentSpanId; private final String correlationId; private final String tenantId; private final Map<String, String> baggage; private final Map<String, Object> customAttributes; private final Instant createdAt; private DistributedContext(Builder builder) { this.traceId = builder.traceId; this.spanId = builder.spanId; this.parentSpanId = builder.parentSpanId; this.correlationId = builder.correlationId; this.tenantId = builder.tenantId; this.baggage = Collections.unmodifiableMap(new HashMap<>(builder.baggage)); this.customAttributes = Collections.unmodifiableMap( new HashMap<>(builder.customAttributes)); this.createdAt = Instant.now(); } public static DistributedContext getCurrent() { DistributedContext context = CURRENT_CONTEXT.get(); if (context == null) { context = createDefault(); CURRENT_CONTEXT.set(context); } return context; } public static void setCurrent(DistributedContext context) { CURRENT_CONTEXT.set(context); } public static DistributedContext create() { return new Builder().build(); } public DistributedContext createChild() { return new Builder() .withTraceId(this.traceId) .withParentSpanId(this.spanId) .withCorrelationId(this.correlationId) .withTenantId(this.tenantId) .withBaggage(this.baggage) .build(); } @Override public void close() { CURRENT_CONTEXT.remove(); } // Builder pattern public static class Builder { private String traceId = IdGenerator.generateTraceId(); private String spanId = IdGenerator.generateSpanId(); private String parentSpanId; private String correlationId = IdGenerator.generateCorrelationId(); private String tenantId = "default"; private Map<String, String> baggage = new HashMap<>(); private Map<String, Object> customAttributes = new HashMap<>(); public Builder withTraceId(String traceId) { this.traceId = traceId; return this; } public Builder withSpanId(String spanId) { this.spanId = spanId; return this; } public Builder withParentSpanId(String parentSpanId) { this.parentSpanId = parentSpanId; return this; } public Builder withCorrelationId(String correlationId) { this.correlationId = correlationId; return this; } public Builder withTenantId(String tenantId) { this.tenantId = tenantId; return this; } public Builder withBaggage(Map<String, String> baggage) { this.baggage.putAll(baggage); return this; } public Builder withBaggageItem(String key, String value) { this.baggage.put(key, value); return this; } public Builder withCustomAttribute(String key, Object value) { this.customAttributes.put(key, value); return this; } public DistributedContext build() { return new DistributedContext(this); } } // Getters public String getTraceId() { return traceId; } public String getSpanId() { return spanId; } public String getParentSpanId() { return parentSpanId; } public String getCorrelationId() { return correlationId; } public String getTenantId() { return tenantId; } public Map<String, String> getBaggage() { return baggage; } public Map<String, Object> getCustomAttributes() { return customAttributes; } public Instant getCreatedAt() { return createdAt; } } Propagation Implementations
1. HTTP Client/Server Propagation
@Component public class HttpContextPropagator { private static final String TRACE_ID = "X-Trace-Id"; private static final String SPAN_ID = "X-Span-Id"; private static final String PARENT_SPAN_ID = "X-Parent-Span-Id"; private static final String CORRELATION_ID = "X-Correlation-Id"; private static final String TENANT_ID = "X-Tenant-Id"; private static final String BAGGAGE_PREFIX = "X-Baggage-"; // Server-side: Extract context from incoming HTTP request @Component public static class ServerContextFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; DistributedContext context = extractContextFromRequest(httpRequest); try (DistributedContext ignored = context) { chain.doFilter(request, response); } } private DistributedContext extractContextFromRequest(HttpServletRequest request) { DistributedContext.Builder builder = DistributedContext.create() .withTraceId(getHeader(request, TRACE_ID, IdGenerator.generateTraceId())) .withSpanId(getHeader(request, SPAN_ID, IdGenerator.generateSpanId())) .withParentSpanId(getHeader(request, PARENT_SPAN_ID, null)) .withCorrelationId(getHeader(request, CORRELATION_ID, IdGenerator.generateCorrelationId())) .withTenantId(getHeader(request, TENANT_ID, "default")); // Extract baggage Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames != null && headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); if (headerName.startsWith(BAGGAGE_PREFIX)) { String key = headerName.substring(BAGGAGE_PREFIX.length()); String value = request.getHeader(headerName); builder.withBaggageItem(key, value); } } return builder.build(); } } // Client-side: Inject context into outgoing HTTP request @Component public static class ClientContextInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { injectContextIntoRequest(request); return execution.execute(request, body); } private void injectContextIntoRequest(HttpRequest request) { DistributedContext context = DistributedContext.getCurrent(); request.getHeaders().set(TRACE_ID, context.getTraceId()); request.getHeaders().set(SPAN_ID, context.getSpanId()); request.getHeaders().set(CORRELATION_ID, context.getCorrelationId()); request.getHeaders().set(TENANT_ID, context.getTenantId()); // Inject baggage context.getBaggage().forEach((key, value) -> { request.getHeaders().set(BAGGAGE_PREFIX + key, value); }); } } private static String getHeader(HttpServletRequest request, String name, String defaultValue) { String value = request.getHeader(name); return value != null ? value : defaultValue; } } 2. Async Messaging Propagation (Kafka)
@Component public class KafkaContextPropagator { private static final String TRACE_ID = "trace_id"; private static final String SPAN_ID = "span_id"; private static final String CORRELATION_ID = "correlation_id"; private static final String TENANT_ID = "tenant_id"; private static final String BAGGAGE_PREFIX = "baggage_"; // Producer: Inject context into Kafka message headers @Component public static class ContextualKafkaProducer { private final KafkaTemplate<String, Object> kafkaTemplate; public void sendMessage(String topic, Object message) { kafkaTemplate.executeInTransaction(operations -> { ProducerRecord<String, Object> record = new ProducerRecord<>(topic, message); injectContextIntoRecord(record); operations.send(record); return null; }); } private void injectContextIntoRecord(ProducerRecord<String, Object> record) { DistributedContext context = DistributedContext.getCurrent(); record.headers() .add(TRACE_ID, context.getTraceId().getBytes(StandardCharsets.UTF_8)) .add(SPAN_ID, context.getSpanId().getBytes(StandardCharsets.UTF_8)) .add(CORRELATION_ID, context.getCorrelationId().getBytes(StandardCharsets.UTF_8)) .add(TENANT_ID, context.getTenantId().getBytes(StandardCharsets.UTF_8)); // Inject baggage context.getBaggage().forEach((key, value) -> { record.headers().add(BAGGAGE_PREFIX + key, value.getBytes(StandardCharsets.UTF_8)); }); } } // Consumer: Extract context from Kafka message headers @Component public static class ContextualKafkaConsumer { @KafkaListener(topics = "${kafka.topic}") public void consume(ConsumerRecord<String, Object> record) { DistributedContext context = extractContextFromRecord(record); try (DistributedContext ignored = context) { processMessage(record.value()); } } private DistributedContext extractContextFromRecord(ConsumerRecord<String, Object> record) { DistributedContext.Builder builder = DistributedContext.create(); // Extract standard context extractHeader(record, TRACE_ID).ifPresent(builder::withTraceId); extractHeader(record, SPAN_ID).ifPresent(builder::withSpanId); extractHeader(record, CORRELATION_ID).ifPresent(builder::withCorrelationId); extractHeader(record, TENANT_ID).ifPresent(builder::withTenantId); // Extract baggage record.headers().headers(BAGGAGE_PREFIX + "*").forEach(header -> { String key = header.key().substring(BAGGAGE_PREFIX.length()); String value = new String(header.value(), StandardCharsets.UTF_8); builder.withBaggageItem(key, value); }); return builder.build(); } private Optional<String> extractHeader(ConsumerRecord<String, Object> record, String headerName) { Header header = record.headers().lastHeader(headerName); if (header != null && header.value() != null) { return Optional.of(new String(header.value(), StandardCharsets.UTF_8)); } return Optional.empty(); } private void processMessage(Object message) { // Process message with context available System.out.println("Processing message with trace: " + DistributedContext.getCurrent().getTraceId()); } } } 3. gRPC Context Propagation
public class GrpcContextPropagator { private static final Metadata.Key<String> TRACE_ID = Metadata.Key.of("x-trace-id", Metadata.ASCII_STRING_MARSHALLER); private static final Metadata.Key<String> SPAN_ID = Metadata.Key.of("x-span-id", Metadata.ASCII_STRING_MARSHALLER); private static final Metadata.Key<String> CORRELATION_ID = Metadata.Key.of("x-correlation-id", Metadata.ASCII_STRING_MARSHALLER); // Server-side interceptor public static class ServerContextInterceptor implements ServerInterceptor { @Override public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall( ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) { DistributedContext context = extractContextFromMetadata(headers); Context grpcContext = Context.current().withValue( ContextKeys.DISTRIBUTED_CONTEXT, context); return Contexts.interceptCall(grpcContext, call, headers, next); } private DistributedContext extractContextFromMetadata(Metadata headers) { DistributedContext.Builder builder = DistributedContext.create(); String traceId = headers.get(TRACE_ID); if (traceId != null) { builder.withTraceId(traceId); } String spanId = headers.get(SPAN_ID); if (spanId != null) { builder.withSpanId(spanId); } String correlationId = headers.get(CORRELATION_ID); if (correlationId != null) { builder.withCorrelationId(correlationId); } return builder.build(); } } // Client-side interceptor public static class ClientContextInterceptor implements ClientInterceptor { @Override public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall( MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) { return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>( next.newCall(method, callOptions)) { @Override public void start(Listener<RespT> responseListener, Metadata headers) { injectContextIntoMetadata(headers); super.start(responseListener, headers); } }; } private void injectContextIntoMetadata(Metadata headers) { DistributedContext context = DistributedContext.getCurrent(); headers.put(TRACE_ID, context.getTraceId()); headers.put(SPAN_ID, context.getSpanId()); headers.put(CORRELATION_ID, context.getCorrelationId()); } } } Advanced Context Management
1. Context Storage and Scoping
public class ContextManager { private static final ThreadLocal<Deque<DistributedContext>> CONTEXT_STACK = ThreadLocal.withInitial(ArrayDeque::new); public static void pushContext(DistributedContext context) { CONTEXT_STACK.get().push(context); } public static DistributedContext popContext() { Deque<DistributedContext> stack = CONTEXT_STACK.get(); return stack.isEmpty() ? null : stack.pop(); } public static DistributedContext getCurrentContext() { Deque<DistributedContext> stack = CONTEXT_STACK.get(); return stack.isEmpty() ? null : stack.peek(); } public static <T> T executeInContext(DistributedContext context, Supplier<T> task) { pushContext(context); try { return task.get(); } finally { popContext(); } } public static void executeInContext(DistributedContext context, Runnable task) { pushContext(context); try { task.run(); } finally { popContext(); } } } // Scoped context for try-with-resources public class ScopedContext implements AutoCloseable { private final DistributedContext previousContext; public ScopedContext(DistributedContext newContext) { this.previousContext = ContextManager.getCurrentContext(); ContextManager.pushContext(newContext); } @Override public void close() { ContextManager.popContext(); if (previousContext != null) { ContextManager.pushContext(previousContext); } } } 2. Context Propagation for Async Operations
public class AsyncContextPropagator { public static <T> CompletableFuture<T> propagateContext(Supplier<CompletableFuture<T>> task) { DistributedContext currentContext = DistributedContext.getCurrent(); return CompletableFuture.supplyAsync(() -> { try (DistributedContext context = currentContext) { return task.get().join(); } }).thenCompose(Function.identity()); } public static ExecutorService contextualExecutorService(ExecutorService delegate) { return new ContextAwareExecutorService(delegate); } private static class ContextAwareExecutorService extends AbstractExecutorService { private final ExecutorService delegate; public ContextAwareExecutorService(ExecutorService delegate) { this.delegate = delegate; } @Override public void execute(Runnable command) { DistributedContext context = DistributedContext.getCurrent(); delegate.execute(() -> { try (DistributedContext ctx = context) { command.run(); } }); } // Delegate other methods @Override public void shutdown() { delegate.shutdown(); } @Override public List<Runnable> shutdownNow() { return delegate.shutdownNow(); } @Override public boolean isShutdown() { return delegate.isShutdown(); } @Override public boolean isTerminated() { return delegate.isTerminated(); } @Override public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { return delegate.awaitTermination(timeout, unit); } } // Reactor Context propagation public static <T> Mono<T> propagateContext(Mono<T> mono) { return Mono.deferContextual(contextView -> { DistributedContext distributedContext = DistributedContext.getCurrent(); return mono.contextWrite(context -> context.put("distributedContext", distributedContext)); }); } public static <T> Flux<T> propagateContext(Flux<T> flux) { return Flux.deferContextual(contextView -> { DistributedContext distributedContext = DistributedContext.getCurrent(); return flux.contextWrite(context -> context.put("distributedContext", distributedContext)); }); } } Integration with Observability Tools
1. OpenTelemetry Integration
@Component public class OpenTelemetryContextPropagator { private final ContextPropagators contextPropagators; public OpenTelemetryContextPropagator() { this.contextPropagators = ContextPropagators.create( TextMapPropagator.composite( W3CTraceContextPropagator.getInstance(), W3CBaggagePropagator.getInstance(), new CustomContextPropagator() ) ); } // Custom context propagator for business context public static class CustomContextPropagator implements TextMapPropagator { private static final String CORRELATION_ID = "x-correlation-id"; private static final String TENANT_ID = "x-tenant-id"; @Override public Collection<String> fields() { return Arrays.asList(CORRELATION_ID, TENANT_ID); } @Override public <C> void inject(Context context, C carrier, TextMapSetter<C> setter) { DistributedContext distContext = ContextKeys.DISTRIBUTED_CONTEXT.get(context); if (distContext != null) { setter.set(carrier, CORRELATION_ID, distContext.getCorrelationId()); setter.set(carrier, TENANT_ID, distContext.getTenantId()); } } @Override public <C> Context extract(Context context, C carrier, TextMapGetter<C> getter) { String correlationId = getter.get(carrier, CORRELATION_ID); String tenantId = getter.get(carrier, TENANT_ID); if (correlationId != null || tenantId != null) { DistributedContext.Builder builder = DistributedContext.create(); if (correlationId != null) builder.withCorrelationId(correlationId); if (tenantId != null) builder.withTenantId(tenantId); return context.with(ContextKeys.DISTRIBUTED_CONTEXT, builder.build()); } return context; } } } 2. MDC (Mapped Diagnostic Context) Integration
@Component public class MDCContextPropagator { private static final String TRACE_ID = "traceId"; private static final String SPAN_ID = "spanId"; private static final String CORRELATION_ID = "correlationId"; private static final String TENANT_ID = "tenantId"; public static void updateMDCFromContext() { DistributedContext context = DistributedContext.getCurrent(); if (context != null) { MDC.put(TRACE_ID, context.getTraceId()); MDC.put(SPAN_ID, context.getSpanId()); MDC.put(CORRELATION_ID, context.getCorrelationId()); MDC.put(TENANT_ID, context.getTenantId()); // Add baggage to MDC context.getBaggage().forEach(MDC::put); } } public static void clearMDC() { MDC.clear(); } @Component public static class MDCContextFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { updateMDCFromContext(); chain.doFilter(request, response); } finally { clearMDC(); } } } } Security Context Propagation
@Component public class SecurityContextPropagator { public void propagateSecurityContext(HttpHeaders headers) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.isAuthenticated()) { String token = generateToken(authentication); headers.set("Authorization", "Bearer " + token); // Propagate user context if (authentication.getPrincipal() instanceof UserDetails) { UserDetails userDetails = (UserDetails) authentication.getPrincipal(); headers.set("X-User-Id", userDetails.getUsername()); headers.set("X-User-Roles", String.join(",", userDetails.getAuthorities() .stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()))); } } } public void extractSecurityContext(HttpServletRequest request) { String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); Authentication authentication = validateToken(token); SecurityContextHolder.getContext().setAuthentication(authentication); } // Extract user context for distributed context String userId = request.getHeader("X-User-Id"); if (userId != null) { DistributedContext current = DistributedContext.getCurrent(); if (current != null) { current.getCustomAttributes().put("userId", userId); } } } private String generateToken(Authentication authentication) { // Implement token generation logic return "generated-token"; } private Authentication validateToken(String token) { // Implement token validation logic return null; } } Testing Distributed Context Propagation
@SpringBootTest @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class ContextPropagationTest { @Autowired private TestRestTemplate restTemplate; @Test public void testHttpContextPropagation() { // Create initial context DistributedContext context = DistributedContext.create() .withTraceId("test-trace-123") .withCorrelationId("test-correlation-456") .withTenantId("test-tenant"); // Execute request with context String result = ContextManager.executeInContext(context, () -> { ResponseEntity<String> response = restTemplate.getForEntity("/api/test", String.class); return response.getBody(); }); // Verify context was propagated assertThat(result).contains("test-trace-123"); } @Test public void testAsyncContextPropagation() throws Exception { DistributedContext context = DistributedContext.create() .withTraceId("async-trace-123"); CompletableFuture<String> future = AsyncContextPropagator.propagateContext(() -> CompletableFuture.supplyAsync(() -> { // This should have the context return DistributedContext.getCurrent().getTraceId(); }) ); String result = future.get(5, TimeUnit.SECONDS); assertThat(result).isEqualTo("async-trace-123"); } @Test public void testKafkaContextPropagation() { DistributedContext context = DistributedContext.create() .withTraceId("kafka-trace-123") .withBaggageItem("businessKey", "test-value"); try (DistributedContext ctx = context) { // Send Kafka message kafkaTemplate.send("test-topic", "test-message"); // Verify context was propagated in consumer // This would typically be verified in integration tests } } } Configuration and Best Practices
1. Spring Boot Auto-Configuration
@Configuration @EnableConfigurationProperties(ContextPropagationProperties.class) public class ContextPropagationAutoConfiguration { @Bean @ConditionalOnMissingBean public HttpContextPropagator.ServerContextFilter serverContextFilter() { return new HttpContextPropagator.ServerContextFilter(); } @Bean @ConditionalOnMissingBean public HttpContextPropagator.ClientContextInterceptor clientContextInterceptor() { return new HttpContextPropagator.ClientContextInterceptor(); } @Bean @ConditionalOnMissingBean public MDCContextPropagator.MDCContextFilter mdcContextFilter() { return new MDCContextPropagator.MDCContextFilter(); } @Bean public AsyncContextPropagator asyncContextPropagator() { return new AsyncContextPropagator(); } } @ConfigurationProperties(prefix = "context.propagation") public class ContextPropagationProperties { private boolean enabled = true; private List<String> propagateHeaders = Arrays.asList( "X-Trace-Id", "X-Span-Id", "X-Correlation-Id", "X-Tenant-Id" ); private Baggage baggage = new Baggage(); // Getters and setters public static class Baggage { private boolean enabled = true; private int maxEntries = 10; private List<String> allowedKeys = new ArrayList<>(); // Getters and setters } } 2. application.yml Configuration
context: propagation: enabled: true propagate-headers: - "X-Trace-Id" - "X-Span-Id" - "X-Correlation-Id" - "X-Tenant-Id" - "X-User-Id" baggage: enabled: true max-entries: 20 allowed-keys: - "business-unit" - "feature-flag" - "user-segment" logging: pattern: level: "%5p [%X{traceId},%X{correlationId},%X{tenantId}]" Performance Considerations
@Component public class ContextPropagationMetrics { private final MeterRegistry meterRegistry; private final Timer contextExtractionTimer; private final Timer contextInjectionTimer; private final Counter contextPropagationErrors; public ContextPropagationMetrics(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; this.contextExtractionTimer = Timer.builder("context.propagation.extraction.time") .description("Time taken to extract context from incoming requests") .register(meterRegistry); this.contextInjectionTimer = Timer.builder("context.propagation.injection.time") .description("Time taken to inject context into outgoing requests") .register(meterRegistry); this.contextPropagationErrors = Counter.builder("context.propagation.errors") .description("Number of context propagation errors") .register(meterRegistry); } public void recordExtraction(Runnable extraction) { contextExtractionTimer.record(extraction); } public void recordInjection(Runnable injection) { contextInjectionTimer.record(injection); } public void recordError() { contextPropagationErrors.increment(); } } Conclusion
Distributed Context Propagation in Java is essential for:
- End-to-end Tracing: Track requests across service boundaries
- Contextual Logging: Include relevant context in log messages
- Business Context: Propagate business-specific information
- Security: Maintain security context across services
- Multi-tenancy: Support tenant isolation in SaaS applications
Key implementation patterns include:
- Using thread-local storage with proper cleanup
- Supporting multiple communication protocols (HTTP, gRPC, messaging)
- Handling asynchronous operations correctly
- Integrating with observability frameworks
- Maintaining performance while ensuring context consistency
This comprehensive approach ensures that contextual information flows seamlessly through your distributed system, enabling better observability, debugging, and user experience.