Distributed Context Propagation in Java

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:

  1. End-to-end Tracing: Track requests across service boundaries
  2. Contextual Logging: Include relevant context in log messages
  3. Business Context: Propagate business-specific information
  4. Security: Maintain security context across services
  5. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper