Series MapLesson 55 / 112
Focus mode active/Press Alt+Shift+R to toggle/Esc to exit
Build CoreOrdered learning track

Distributed Context Propagation

Context Propagation across HTTP Kafka and Executors

Propagasi context lintas boundary sinkron dan asinkron: HTTP, Kafka, executor, thread pool, MDC, Trace Context, Baggage, correlation ID, causation ID, dan ThreadLocal risk

6 min read1171 words
PrevNext
Lesson 55112 lesson track22–61 Build Core
#context-propagation#opentelemetry#http#kafka+4 more

Part 055 — Context Propagation across HTTP, Kafka, and Executors

Fokus part ini: memahami bagaimana context berpindah dari satu boundary ke boundary lain di sistem enterprise: inbound HTTP, outbound HTTP, Kafka producer/consumer, executor/thread pool, MDC logging, security context, OpenTelemetry trace context, baggage, correlation ID, dan causation ID.

Context propagation adalah salah satu sumber bug production yang paling sulit dilihat.

Bukan karena konsepnya rumit, tetapi karena saat gagal, aplikasi tetap berjalan.

Yang rusak adalah kemampuan kita menjawab:

Request ini berasal dari mana?
User/tenant mana yang memicunya?
Span mana parent-nya?
Event Kafka ini disebabkan oleh HTTP command apa?
Log ini milik request yang mana?
Kenapa audit trail terputus?
Kenapa trace tampak seperti beberapa request terpisah?

Dalam JAX-RS enterprise service, context sering berpindah melalui:

HTTP inbound request
  -> JAX-RS filter/resource method
  -> service/domain layer
  -> database call
  -> outbound HTTP call
  -> Kafka publish
  -> async executor
  -> Kafka consumer in another service
  -> reconciliation job

Jika propagation tidak disiplin, debugging distributed system menjadi tebakan.


1. Core Mental Model

Context adalah metadata yang menjelaskan eksekusi, bukan data bisnis utama.

Contoh context:

trace_id
span_id
parent_span_id
correlation_id
causation_id
request_id
tenant_id
user_id / subject
service_name
operation_name
idempotency_key
feature_flag_snapshot
locale / timezone
security principal

Tidak semua context boleh dipropagasikan ke semua tempat.

Aturan senior engineer:

Propagate what is required for correctness, security, and observability.
Do not propagate sensitive data blindly.

Context propagation harus punya boundary.

flowchart LR C[Client] -->|HTTP headers| API[JAX-RS API] API -->|MDC + OTel Context| SVC[Service Layer] SVC -->|HTTP headers| DOWN[Downstream Service] SVC -->|Kafka headers| TOPIC[Kafka Topic] TOPIC -->|Kafka headers| CONS[Consumer Service] SVC -->|Context wrapper| EXEC[Executor Task]

Yang perlu dipahami:

  1. HTTP membawa context lewat header.
  2. Kafka membawa context lewat header.
  3. Thread pool tidak otomatis membawa ThreadLocal.
  4. MDC biasanya berbasis ThreadLocal.
  5. OpenTelemetry context juga harus disimpan/diaktifkan dengan benar.
  6. Security context tidak boleh disalin sembarangan ke background job.

2. Trace Context vs Correlation ID vs Causation ID

Ketiganya sering dicampur.

Trace context

Trace context dipakai distributed tracing.

Biasanya mengikuti W3C Trace Context:

traceparent: 00-<trace-id>-<span-id>-<flags>
tracestate: vendor-specific state

Maknanya:

trace_id -> satu distributed execution graph
span_id  -> satu operation/node dalam graph

Trace context cocok untuk observability.

Correlation ID

Correlation ID adalah ID yang dipakai untuk mengelompokkan log/event terkait.

Dalam banyak organisasi, correlation ID bisa sama dengan trace ID, tetapi tidak wajib.

Contoh:

X-Correlation-ID: quote-request-8f37...

Gunanya:

Cari semua log yang terkait satu external request/business operation.

Causation ID

Causation ID menjelaskan hubungan sebab-akibat.

Contoh:

HTTP command CreateQuote
  correlation_id = c-123
  causation_id   = request-001

Kafka event QuoteCreated
  correlation_id = c-123
  causation_id   = event-quote-created-001

Kafka event PricingRequested
  correlation_id = c-123
  causation_id   = event-quote-created-001

Causation ID sangat penting dalam event-driven system.

Tanpa causation ID, kita tahu event-event saling terkait, tetapi tidak tahu event mana menyebabkan event berikutnya.


3. Context Carrier by Boundary

Setiap boundary punya carrier berbeda.

BoundaryCarrierCommon Fields
HTTP inboundHTTP headerstraceparent, tracestate, x-correlation-id, auth header
HTTP outboundHTTP headerstrace context, correlation ID, tenant context jika aman
Kafka producerKafka record headerstrace context, correlation ID, causation ID, event metadata
Kafka consumerKafka record headersrestored context, event ID, source service
Executor/thread poolcaptured object/wrapperOTel context, MDC map, security-safe subset
LogsMDC fieldstrace ID, span ID, correlation ID, tenant ID if allowed
Auditexplicit audit payloadactor, action, target, timestamp, reason

Prinsipnya:

Never assume context crosses boundary automatically.

4. Inbound HTTP Context in JAX-RS

Pada inbound HTTP, context biasanya dibaca di filter.

Contoh JAX-RS filter konseptual:

@Provider
@Priority(Priorities.AUTHENTICATION)
public final class RequestContextFilter implements ContainerRequestFilter, ContainerResponseFilter {

    @Override
    public void filter(ContainerRequestContext request) {
        String correlationId = firstNonBlank(
            request.getHeaderString("X-Correlation-ID"),
            UUID.randomUUID().toString()
        );

        MDC.put("correlation_id", correlationId);
        MDC.put("http_method", request.getMethod());
        MDC.put("http_path", request.getUriInfo().getPath());

        // Do not blindly trust tenant/user headers unless gateway/auth policy says so.
    }

    @Override
    public void filter(ContainerRequestContext request, ContainerResponseContext response) {
        String correlationId = MDC.get("correlation_id");
        if (correlationId != null) {
            response.getHeaders().putSingle("X-Correlation-ID", correlationId);
        }
        MDC.clear();
    }
}

Catatan penting:

MDC.clear() wajib dilakukan.

Alasannya: thread request bisa dipakai ulang oleh container.

Jika MDC tidak dibersihkan, request berikutnya bisa mewarisi context lama.

Itu bukan hanya bug observability; itu bisa menjadi data leak.


5. HTTP Outbound Propagation

Saat service memanggil downstream service, context harus dipropagasikan secara eksplisit.

Contoh konseptual:

public final class OutboundHeaderFactory {
    public Map<String, String> buildHeaders(RequestContext ctx) {
        Map<String, String> headers = new HashMap<>();
        headers.put("X-Correlation-ID", ctx.correlationId());
        headers.put("X-Request-ID", UUID.randomUUID().toString());

        // W3C trace context is usually injected by OpenTelemetry instrumentation.
        // If manual propagation is used, verify exact propagator standard.

        return headers;
    }
}

Anti-pattern:

// BAD: forward all inbound headers blindly
inboundHeaders.forEach(outboundRequest::header);

Kenapa buruk?

Authorization header bisa bocor ke downstream yang tidak berhak.
Internal-only header bisa keluar trust boundary.
Spoofed tenant header bisa dipakai oleh service lain.
Large headers bisa menyebabkan 431 Request Header Fields Too Large.

Better rule:

Whitelist propagated headers.

Untuk enterprise systems, header propagation harus dianggap security-sensitive.


6. Kafka Header Propagation

Kafka tidak punya HTTP headers, tetapi record punya headers.

Context bisa dibawa lewat Kafka record headers:

traceparent
tracestate
x-correlation-id
x-causation-id
event-id
event-type
event-version
source-service
tenant-id, jika policy mengizinkan

Producer harus mengisi metadata event secara eksplisit.

ProducerRecord<String, byte[]> record = new ProducerRecord<>(topic, key, payload);
record.headers().add("x-correlation-id", correlationId.getBytes(StandardCharsets.UTF_8));
record.headers().add("x-causation-id", commandId.getBytes(StandardCharsets.UTF_8));
record.headers().add("event-id", eventId.getBytes(StandardCharsets.UTF_8));
record.headers().add("source-service", "quote-order-service".getBytes(StandardCharsets.UTF_8));
producer.send(record);

Dalam praktik production, jangan menulis helper manual berulang di semua producer.

Gunakan abstraction:

EventPublisher
  -> enrich metadata
  -> inject trace context
  -> enforce event ID
  -> enforce event type/version
  -> publish

7. Kafka Consumer Context Restoration

Saat consumer menerima event, context harus direstore untuk logging dan tracing selama processing event.

public void onMessage(ConsumerRecord<String, byte[]> record) {
    try (MdcScope ignored = MdcScope.fromKafka(record.headers())) {
        String correlationId = Mdc.get("correlation_id");
        log.info("Processing event topic={} partition={} offset={}",
            record.topic(), record.partition(), record.offset());

        handler.handle(record);
    }
}

Yang harus terjadi:

Read Kafka headers.
Restore correlation_id into MDC.
Restore trace context into OTel context if instrumentation/manual propagation supports it.
Create consumer span.
Clear MDC after processing.
Commit offset only after safe processing boundary.

Failure mode umum:

Consumer logs tidak punya correlation ID.
Trace baru dibuat untuk setiap consumer processing, tidak tersambung dengan producer.
MDC dari message sebelumnya bocor ke message berikutnya.
DLQ event kehilangan metadata original.
Retry topic membuat causation chain putus.

8. Executor and Thread Pool Propagation

Thread pool adalah tempat context sering hilang.

Contoh:

MDC.put("correlation_id", "c-123");
executor.submit(() -> {
    log.info("Running async task"); // correlation_id might be missing
});

Kenapa?

Karena MDC biasanya ThreadLocal.

Thread worker berbeda dari request thread.

Solusinya bukan berharap framework otomatis selalu benar.

Solusinya capture dan restore.

public final class ContextAwareExecutor implements Executor {
    private final Executor delegate;

    public ContextAwareExecutor(Executor delegate) {
        this.delegate = delegate;
    }

    @Override
    public void execute(Runnable command) {
        Map<String, String> capturedMdc = MDC.getCopyOfContextMap();
        io.opentelemetry.context.Context capturedOtel = io.opentelemetry.context.Context.current();

        delegate.execute(() -> {
            Map<String, String> previous = MDC.getCopyOfContextMap();
            try (var scope = capturedOtel.makeCurrent()) {
                if (capturedMdc != null) {
                    MDC.setContextMap(capturedMdc);
                } else {
                    MDC.clear();
                }
                command.run();
            } finally {
                MDC.clear();
                if (previous != null) {
                    MDC.setContextMap(previous);
                }
            }
        });
    }
}

Catatan:

Security context propagation ke async task harus dibatasi.

Background work tidak selalu boleh memakai identity user original.

Kadang harus berubah menjadi service identity dengan audit reference ke original actor.


9. ThreadLocal Risk

ThreadLocal berguna, tetapi berbahaya di server.

Risiko utama:

Context leak antar request.
Memory leak jika value besar.
Data user/tenant bocor ke request lain.
Async task kehilangan context.
Test menjadi flaky karena state tersembunyi.

Anti-pattern:

public final class TenantContextHolder {
    private static final ThreadLocal<String> TENANT = new ThreadLocal<>();

    public static void set(String tenant) { TENANT.set(tenant); }
    public static String get() { return TENANT.get(); }
}

Pattern ini bisa diterima hanya jika:

Set di boundary yang jelas.
Clear di finally/filter.
Tidak dipakai sebagai pengganti parameter domain.
Tidak dipakai lintas async boundary tanpa wrapper.
Tidak menyimpan object besar.

Preferensi desain:

Use explicit context object for business-sensitive decisions.
Use MDC/OTel context for observability metadata.

10. Baggage: Powerful but Dangerous

OpenTelemetry Baggage adalah key-value metadata yang bisa dipropagasikan lintas service.

Baggage berguna untuk:

tenant tier
routing hint
experiment cohort
region hint

Tetapi baggage berbahaya jika berisi:

PII
pricing data
large payload
authorization decision
secret
full user profile

Rule:

Baggage is not a secure data store.
Baggage is not an authorization source.
Baggage should be small, low-cardinality, and policy-approved.

Internal system harus punya allowlist baggage keys.


11. Context Propagation and Security

Tidak semua context dari inbound request boleh dipercaya.

Contoh raw header:

X-Tenant-ID: tenant-a
X-User-ID: admin
X-Correlation-ID: c-123

Header ini bisa datang dari client langsung kecuali gateway menghapus/mengganti.

Security rule:

Identity and tenant context must come from trusted authentication/authorization layer, not arbitrary client headers.

Header yang boleh diterima dari client:

Correlation/request ID, jika divalidasi dan dibatasi panjang/formatnya.

Header yang harus berasal dari trusted layer:

authenticated subject
tenant identity
permission claims
service identity

Internal verification harus memastikan gateway/proxy/filter tidak membiarkan spoofed identity headers lolos.


12. Context Propagation in JAX-RS Filters

Pipeline yang umum:

sequenceDiagram participant Client participant Gateway participant Filter as JAX-RS Request Filter participant Resource participant Service participant Downstream Client->>Gateway: HTTP request Gateway->>Filter: headers normalized Filter->>Filter: extract trace/correlation/security-safe context Filter->>Resource: resource method Resource->>Service: explicit request context Service->>Downstream: propagate allowlisted headers Downstream-->>Service: response Service-->>Resource: result Resource-->>Filter: response Filter-->>Gateway: add response correlation header + cleanup

Filter responsibilities:

Generate missing correlation ID.
Validate incoming correlation ID format/length.
Populate MDC.
Ensure response includes correlation ID.
Clear MDC in all outcomes.
Do not decide business authorization unless designed as auth filter.

13. Context Propagation in Event-Driven Flow

Event-driven flow needs causation tracking.

sequenceDiagram participant API as Quote API participant DB as PostgreSQL participant Kafka participant Pricing as Pricing Consumer participant Audit as Audit Consumer API->>DB: persist quote command + outbox API->>Kafka: publish QuoteCreated with correlation/causation Kafka->>Pricing: consume QuoteCreated Pricing->>Kafka: publish PricingRequested with same correlation, new causation Kafka->>Audit: consume events and build timeline

Event metadata should usually include:

event_id
correlation_id
causation_id
traceparent, if supported
event_type
event_version
occurred_at
source_service
source_instance, optional
producer_schema_version

Do not rely only on Kafka topic/offset for business event identity.

Offset identifies position in a partition, not business event identity.


14. DLQ and Retry Must Preserve Context

Retry/DLQ often breaks context.

Bad behavior:

Original event goes to retry topic.
Retry publisher creates new event headers from scratch.
DLQ contains payload only, no original metadata.
Operator cannot trace original HTTP request.

Expected behavior:

Original correlation ID preserved.
Original event ID preserved or referenced.
Retry attempt count added.
Last failure reason added safely.
Original topic/partition/offset captured.
DLQ record keeps diagnostic metadata.

Example retry metadata:

x-original-topic
x-original-partition
x-original-offset
x-original-event-id
x-correlation-id
x-causation-id
x-retry-attempt
x-last-error-code
x-failed-at

Avoid putting full exception stack trace into Kafka headers.

It can exceed size limits and leak sensitive data.


15. Context Object vs Global Access

A mature codebase often uses explicit context object:

public record RequestContext(
    String correlationId,
    String requestId,
    String tenantId,
    String subject,
    Instant receivedAt
) {}

Resource method:

@POST
@Path("/quotes")
public Response createQuote(CreateQuoteRequest request, @Context HttpHeaders headers) {
    RequestContext ctx = requestContextFactory.from(headers);
    QuoteResult result = quoteService.createQuote(ctx, request);
    return Response.status(Status.CREATED).entity(result).build();
}

Benefit:

Business-relevant context is explicit.
Tests can construct context easily.
No hidden dependency on ThreadLocal.
Context crossing async boundary becomes intentional.

But avoid passing an overloaded Context object containing everything.

Context object should be stable and intentionally scoped.


16. Context and Tenant-Aware Systems

In enterprise CPQ/order systems, tenant context can affect:

product catalog
pricing rules
tax rules
currency
permissions
data partition
feature flags
routing
logging/audit

Therefore tenant context is not merely observability metadata.

Tenant context may affect correctness.

Rule:

Tenant used for authorization/data access must come from trusted identity/config boundary.
Tenant used for logs must be sanitized and policy-approved.

Do not derive tenant from arbitrary request body unless the API contract explicitly allows it and authorization verifies it.


17. Debugging Broken Context Propagation

Symptoms:

Logs have missing correlation_id.
Trace shows multiple roots for one business operation.
Kafka consumer spans are disconnected.
DLQ messages cannot be traced to original request.
Tenant ID appears wrong in logs.
MDC fields from previous request appear in current request.

Debug path:

1. Check inbound HTTP headers.
2. Check request filter extraction.
3. Check MDC populated at resource method.
4. Check outbound HTTP/Kafka headers.
5. Check executor boundary capture/restore.
6. Check consumer header extraction.
7. Check MDC cleanup.
8. Check OTel propagator configuration.
9. Check gateway/proxy header mutation.
10. Check log formatter includes fields.

Useful test:

Send request with known X-Correlation-ID.
Assert response contains same correlation ID.
Assert logs include same correlation ID.
Assert outbound HTTP mock receives same correlation ID.
Assert Kafka record headers include same correlation ID.
Assert consumer logs include same correlation ID.

18. Testing Context Propagation

Unit tests should cover:

missing correlation ID generates one
invalid correlation ID rejected or normalized
MDC cleared after request
outbound header allowlist applied
Kafka headers include required metadata
executor wrapper preserves MDC/OTel context
security-sensitive headers are not forwarded

Example test shape:

@Test
void clearsMdcAfterRequest() {
    filter.filter(requestContext);
    assertThat(MDC.get("correlation_id")).isEqualTo("c-123");

    filter.filter(requestContext, responseContext);
    assertThat(MDC.getCopyOfContextMap()).isNullOrEmpty();
}

Integration tests should verify propagation across real components where possible:

JAX-RS test container
mock downstream HTTP server
Testcontainers Kafka
log capture/test appender

19. Common Failure Modes

FailureCauseImpactDetection
Missing correlation IDFilter not registeredLogs cannot be groupedLog query/dashboard
Wrong tenant in logsMDC not clearedData leak riskSecurity review/log anomaly
Trace disconnectedPropagator not configuredBroken distributed traceTrace UI roots
Kafka context missingHeaders not copiedEvent debugging hardConsumer log/header inspection
Async logs missing contextThreadLocal not propagatedBackground task invisibleExecutor tests
Header spoofingTrusting inbound headersSecurity breachPen test/security review
Cardinality explosionTenant/user in metric labelObservability cost/instabilityMetrics backend warning
DLQ context lostRetry publisher drops headersIncident diagnosis slowDLQ inspection

20. PR Review Checklist

Review context propagation changes with these questions:

Does inbound filter validate and normalize correlation/request IDs?
Is MDC cleared in all code paths?
Are propagated HTTP headers allowlisted?
Is trace context propagated by OTel instrumentation or manual propagator?
Are Kafka headers populated consistently?
Are retry/DLQ headers preserving diagnostic metadata?
Does executor/thread pool capture and restore context?
Is security/tenant context sourced from trusted boundary?
Are sensitive values excluded from baggage/logs/headers?
Are tests covering missing/invalid/leaked context?

Red flags:

forward all headers
static ThreadLocal without clear
business logic reading tenant from MDC
Kafka event without event_id/correlation_id
async task using user identity indefinitely
MDC fields not included in log pattern

21. Internal Verification Checklist

For CSG Quote & Order or any internal enterprise codebase, verify instead of assuming:

HTTP Context
- What inbound headers are standardized?
- Is W3C Trace Context used?
- Is X-Correlation-ID used separately from trace ID?
- Does gateway generate/normalize request IDs?
- Are spoofed identity/tenant headers stripped?

JAX-RS/Jersey Layer
- Which request/response filters populate MDC?
- Are filters registered explicitly or discovered by scanning?
- Is MDC cleared in response filter/finally block?
- Are ExceptionMappers preserving response correlation headers?

OpenTelemetry
- Is OTel Java agent used or manual SDK instrumentation?
- Which propagators are enabled?
- Are traces, metrics, and logs correlated?
- Is baggage enabled or restricted?

Kafka
- Which headers are mandatory for events?
- Are traceparent/correlation/causation IDs propagated?
- Are retry/DLQ topics preserving original metadata?
- Is event_id required?

Executors
- Are custom executors wrapped for MDC/OTel context?
- Are async jobs allowed to carry user context?
- Is security context propagated, transformed, or dropped?

Tenancy
- Where is tenant resolved?
- Is tenant context trusted from token, gateway, database, or request payload?
- Are tenant IDs logged? If yes, is this policy-approved?

Operations
- Can one production incident timeline be reconstructed from logs/traces/events?
- Are runbooks using correlation ID and trace ID consistently?

22. Senior Engineering Heuristics

Use these rules:

Context that affects correctness should be explicit.
Context that affects observability can live in MDC/OTel context.
Context that affects security must come from trusted identity boundary.
Context crossing async boundary must be captured/restored intentionally.
Context crossing service boundary must be allowlisted.
Context in Kafka must survive retry and DLQ.
Context must be cleaned up after use.

The goal is not to add IDs everywhere.

The goal is causal continuity.

A senior engineer should be able to answer:

Given one customer-impacting issue, can we reconstruct the technical and business path across HTTP, DB, Kafka, jobs, and downstream services?

If the answer is no, context propagation is incomplete.


23. Practical Mental Model

Think of context propagation as a chain of custody.

Inbound request receives context.
Service validates and normalizes context.
Business layer uses explicit safe context.
Observability layer records context.
Outbound integrations propagate allowed context.
Async/event boundaries preserve causality.
Cleanup prevents leakage.

The invariant:

Every externally triggered operation should be traceable without leaking sensitive data or trusting untrusted metadata.

That is the bar for production enterprise JAX-RS systems.

Lesson Recap

You just completed lesson 55 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.