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

Threading and Request Context

Threading Request Context and Context Propagation

Request context, ThreadLocal, async boundary, MDC, security context, and context propagation risk in JAX-RS enterprise services

6 min read1126 words
PrevNext
Lesson 33112 lesson track22–61 Build Core
#threading#context-propagation#jax-rs#concurrency+4 more

Part 033 — Threading, Request Context, and Context Propagation

Fokus part ini: memahami bagaimana request context hidup di dalam thread, apa yang terjadi ketika execution berpindah thread, dan mengapa context propagation adalah fondasi untuk logging, tracing, security, tenancy, audit, dan debugging production.

Di service JAX-RS enterprise, bug context propagation sering tidak terlihat sebagai compile error. Bug ini muncul sebagai:

- log kehilangan correlation ID
- trace terputus di tengah request
- user identity hilang di async task
- tenant ID salah atau null
- audit event tidak bisa dikaitkan dengan command asal
- security check berjalan tanpa principal yang benar
- transaction/request-scoped object dipakai di thread yang salah

Senior engineer harus melihat request tidak hanya sebagai method call, tetapi sebagai execution context yang bergerak melewati banyak boundary.

HTTP request -> container thread -> JAX-RS resource -> service layer
             -> database/client/cache/event publisher
             -> executor/Kafka callback/async worker

Jika context tidak dibawa dengan benar, sistem masih bisa terlihat bekerja, tetapi kehilangan auditability dan debuggability.


1. Core Mental Model

Request processing di Java backend biasanya dimulai dari satu thread container.

sequenceDiagram autonumber participant Client participant Runtime as HTTP/Servlet/JAX-RS Runtime participant T as Request Thread participant Resource as JAX-RS Resource participant Service participant DB Client->>Runtime: HTTP request Runtime->>T: assign request thread T->>Resource: invoke resource method Resource->>Service: call business/application service Service->>DB: execute query/transaction DB-->>Service: result Service-->>Resource: response DTO Resource-->>Runtime: Response Runtime-->>Client: HTTP response

Pada alur sederhana, semua berjalan di thread yang sama. Context dapat diakses dari:

- JAX-RS @Context
- SecurityContext
- HttpServletRequest, jika running on Servlet container
- MDC/logging context
- ThreadLocal yang dibuat framework atau aplikasi
- OpenTelemetry current Context
- transaction context, tergantung framework

Masalah muncul saat alur berpindah dari satu thread ke thread lain.

flowchart TD A[Request Thread] --> B[JAX-RS Resource] B --> C[Application Service] C --> D{Async boundary?} D -->|No| E[Same Thread Context Intact] D -->|Yes| F[Executor Thread] F --> G{Context propagated?} G -->|Yes| H[Logs/Trace/Security/Tenant Intact] G -->|No| I[Lost Correlation, Identity, Tenant, Trace]

Principle:

Context is not business data, but it controls whether business data can be trusted, traced, secured, and audited.

2. What Is Request Context?

Request context adalah kumpulan metadata runtime yang menjelaskan request yang sedang diproses.

Contoh:

Context itemFungsi
request IDIdentifier request lokal pada service
correlation IDMengaitkan request lintas service
causation IDMenjelaskan event/command penyebab aksi berikutnya
trace IDDistributed trace identifier
span IDUnit kerja spesifik dalam trace
user principalIdentity user/service caller
tenant IDBoundary data dan config tenant
locale/timezoneFormatting dan interpretation context
auth token claimsSubject, audience, scopes, roles
client metadatachannel, app version, source system
deadline/timeoutBatas waktu request
feature flag contextFlag targeting dan rollout context
audit contextActor, action, origin, reason

Tidak semua context harus diteruskan ke semua tempat. Tetapi context yang diperlukan untuk correctness harus jelas.

Bad smell:

"Service ini butuh tenant ID, tapi tenant ID diambil dari ThreadLocal tersembunyi."

Lebih aman:

- tenant ID eksplisit di command/application context
- logging/tracing context tetap bisa ada di MDC/OTel context
- security context tidak disalin sembarangan ke background job

3. JAX-RS @Context

JAX-RS menyediakan @Context untuk mengakses runtime context.

Contoh object umum:

@Path("/quotes")
public class QuoteResource {

    @Context
    private UriInfo uriInfo;

    @Context
    private HttpHeaders headers;

    @Context
    private SecurityContext securityContext;

    @GET
    @Path("/{id}")
    public Response getQuote(@PathParam("id") String id) {
        String path = uriInfo.getPath();
        String user = securityContext.getUserPrincipal().getName();
        return Response.ok().build();
    }
}

Important distinction:

@Context gives access to request/runtime metadata.
It is not a replacement for explicit application command data.

Avoid passing UriInfo, HttpHeaders, or SecurityContext deep into domain logic.

Better boundary:

public record RequestContext(
    String correlationId,
    String causationId,
    String tenantId,
    String actorId,
    String sourceSystem
) {}

Resource layer extracts transport/runtime details, then passes explicit application context.

@POST
public Response submitQuote(SubmitQuoteRequest request) {
    RequestContext ctx = contextFactory.fromCurrentRequest();
    QuoteResult result = quoteApplicationService.submit(ctx, request.toCommand());
    return Response.accepted(result).build();
}

Senior-level rule:

JAX-RS context should be consumed at the edge and translated into explicit application context.

4. ThreadLocal: Useful, Dangerous, and Often Invisible

ThreadLocal stores data attached to the current thread.

private static final ThreadLocal<String> TENANT = new ThreadLocal<>();

Common use cases:

- logging MDC
- current user
- current tenant
- current transaction
- OpenTelemetry current span/context
- framework request scope internals

The danger: request threads are reused.

sequenceDiagram autonumber participant Req1 as Request A participant T as Container Thread participant Req2 as Request B Req1->>T: set ThreadLocal tenant=A Req1->>T: processing fails before clear Req2->>T: same thread reused Req2->>T: reads stale tenant=A

Critical rule:

Every ThreadLocal set must have a guaranteed clear in finally block.

Example:

public void filter(ContainerRequestContext requestContext) {
    try {
        TenantContext.set(resolveTenant(requestContext));
        // continue chain depending on filter model
    } finally {
        TenantContext.clear();
    }
}

In JAX-RS filters, the exact lifecycle depends on filter implementation and runtime. Verify carefully.

Preferred approach:

- Use ThreadLocal for cross-cutting context only when framework-compatible.
- Never rely on ThreadLocal as the only source of business-critical data.
- Clear aggressively.
- Wrap executor tasks if context must cross threads.

5. MDC Propagation

MDC, or Mapped Diagnostic Context, lets logs automatically include request metadata.

Typical log fields:

correlation_id
causation_id
trace_id
span_id
tenant_id
actor_id
request_path
http_method
source_system

Example conceptual filter:

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

    @Override
    public void filter(ContainerRequestContext request) {
        String correlationId = getOrCreateCorrelationId(request);
        MDC.put("correlation_id", correlationId);
    }

    @Override
    public void filter(ContainerRequestContext request, ContainerResponseContext response) {
        try {
            response.getHeaders().putSingle("X-Correlation-ID", MDC.get("correlation_id"));
        } finally {
            MDC.clear();
        }
    }
}

Risk:

MDC is usually ThreadLocal-backed.
If execution moves to another thread, MDC may not follow.
If MDC is not cleared, next request may inherit stale values.

Executor example without propagation:

executor.submit(() -> {
    log.info("publishing quote event"); // correlation_id may be missing
});

Executor example with explicit capture:

Map<String, String> captured = MDC.getCopyOfContextMap();

executor.submit(() -> {
    Map<String, String> previous = MDC.getCopyOfContextMap();
    try {
        if (captured != null) {
            MDC.setContextMap(captured);
        }
        log.info("publishing quote event");
    } finally {
        MDC.clear();
        if (previous != null) {
            MDC.setContextMap(previous);
        }
    }
});

In production code, avoid hand-rolling this repeatedly. Prefer a standard executor wrapper or platform-approved context propagation library.


6. SecurityContext and Principal Propagation

JAX-RS SecurityContext represents caller identity and security state for the current request.

@Context
SecurityContext securityContext;

Typical questions:

- Who is the caller?
- Is the request secure?
- Does caller have role X?
- Is caller a user, service, or internal platform component?

Do not blindly propagate full security context to async/background work.

Why?

- request token may expire
- user may no longer be authorized later
- background work may need service identity, not user identity
- replayed job should be auditable as system action caused by user command

Better model:

actor_id       -> who initiated
subject_type   -> user/service/system
causation_id   -> command/event that caused the work
service_identity -> identity used for outbound service call

For async jobs and Kafka consumers, prefer explicit audit context over pretending the original HTTP security context is still alive.


7. Tenant Context Propagation

In enterprise systems, tenant context can affect:

- routing
- authorization
- data access
- product catalog
- pricing rules
- feature flags
- logging
- audit
- metrics segmentation

Dangerous pattern:

String tenantId = TenantThreadLocal.get();
quoteRepository.findById(quoteId); // repository implicitly filters by ThreadLocal tenant

Safer pattern:

quoteRepository.findByTenantAndId(ctx.tenantId(), quoteId);

A balanced approach is often used:

- explicit tenant in application/domain data access path
- MDC/telemetry tenant field for debugging
- validation guard that rejects missing tenant at boundary

Critical invariant:

No tenant-sensitive operation should succeed without a known tenant boundary.

PR review questions:

[ ] Where is tenant resolved?
[ ] Is tenant trusted from header, token claim, path, gateway, or config?
[ ] Is tenant validated against actor permission?
[ ] Does repository query enforce tenant boundary?
[ ] Does async/event processing preserve tenant context?
[ ] Are logs tenant-aware without leaking PII?

8. Async Boundary Types

Context can be lost at many boundaries.

BoundaryRisk
ExecutorService.submitMDC/ThreadLocal/OTel context lost
CompletableFuture.supplyAsyncRuns on another pool, context lost
scheduled jobNo HTTP request context exists
Kafka consumerNew event context, not HTTP context
HTTP client callbackDifferent callback thread
reactive streamcontext may require framework-specific propagation
servlet asyncrequest lifecycle becomes more complex
Jersey async responsecontext availability must be verified

Typical failure:

CompletableFuture.supplyAsync(() -> service.loadSomething());

Problems:

- Which executor is used?
- Does it propagate MDC?
- Does it propagate OpenTelemetry context?
- Does it have timeout/cancellation?
- Does it respect tenant/security context?
- Does it have bounded queue?

Senior-level rule:

Never introduce async execution without defining context, timeout, cancellation, queue, error, and observability behavior.

9. Context Propagation Across Executors

A production-grade executor wrapper usually handles:

- MDC capture/restore/clear
- OpenTelemetry context capture/restore
- correlation ID preservation
- tenant/audit context preservation, if allowed
- exception logging
- task duration metrics
- queue metrics
- rejection handling

Conceptual wrapper:

public final class ContextAwareExecutor {
    private final ExecutorService delegate;

    public void submit(Runnable task) {
        Map<String, String> mdc = MDC.getCopyOfContextMap();
        RequestContext requestContext = RequestContextHolder.currentOrNull();

        delegate.submit(() -> {
            try {
                if (mdc != null) MDC.setContextMap(mdc);
                RequestContextHolder.set(requestContext);
                task.run();
            } finally {
                RequestContextHolder.clear();
                MDC.clear();
            }
        });
    }
}

This example is intentionally simplified. Real implementation must also account for:

- previous context on worker thread
- rejected execution
- cancellation
- nested async tasks
- OTel Context
- task naming
- metrics
- security restrictions

10. Context Propagation Across HTTP

For outbound HTTP calls, propagate the minimum required context.

Common headers:

traceparent
tracestate
X-Correlation-ID
X-Causation-ID
X-Request-ID
X-Tenant-ID, if allowed by platform standard
Authorization, if doing token forwarding or token exchange

Do not invent header propagation casually.

Governance questions:

[ ] Which headers are official?
[ ] Which headers are gateway-owned?
[ ] Which headers are internal-only?
[ ] Which headers are user-controllable and must not be trusted?
[ ] Which headers must be stripped at boundary?
[ ] Which headers are propagated to external vendors?

Example Jersey client filter concept:

public class OutboundContextFilter implements ClientRequestFilter {
    @Override
    public void filter(ClientRequestContext requestContext) {
        requestContext.getHeaders().putSingle("X-Correlation-ID", Current.correlationId());
        requestContext.getHeaders().putSingle("X-Causation-ID", Current.causationId());
    }
}

Security warning:

Never trust inbound tenant/user headers unless they are asserted by a trusted gateway or validated against a token claim.

11. Context Propagation Across Kafka

Kafka has no HTTP headers, but records can carry headers.

Useful event headers:

correlation_id
causation_id
traceparent
tenant_id
event_id
producer_service
schema_version
command_id

Conceptual producer:

ProducerRecord<String, QuoteSubmittedEvent> record =
    new ProducerRecord<>("quote.submitted.v1", key, event);

record.headers().add("correlation_id", bytes(ctx.correlationId()));
record.headers().add("causation_id", bytes(ctx.causationId()));
record.headers().add("tenant_id", bytes(ctx.tenantId()));

Consumer should create a new processing context from event headers.

HTTP request context and Kafka processing context are related, not identical.

Causation chain:

HTTP command correlation_id = C1
HTTP command causation_id   = request-id-123
published event_id          = E1
consumer causation_id       = E1
consumer correlation_id     = C1

This lets you answer:

- Which request caused this event?
- Which event caused this downstream mutation?
- Which tenant was affected?
- Which service produced the event?

12. OpenTelemetry Current Context

OpenTelemetry often stores the current context in a thread-local-like mechanism.

Conceptually:

current span/context is associated with current execution thread

When execution crosses thread boundary, the current span may not follow unless instrumentation handles it.

Executor example concept:

Context captured = Context.current();

executor.submit(() -> {
    try (Scope scope = captured.makeCurrent()) {
        doWork();
    }
});

You do not need to manually instrument everything if the platform uses Java agent/instrumentation. But you must verify:

[ ] Does HTTP server instrumentation exist?
[ ] Does Jersey/JAX-RS instrumentation exist?
[ ] Does HTTP client instrumentation exist?
[ ] Does Kafka producer/consumer instrumentation exist?
[ ] Does executor instrumentation exist?
[ ] Does custom thread pool preserve context?

Part 054–056 will go deeper into OpenTelemetry, sampling, and high-cardinality risks.


13. Request Scope vs Application Scope

Request-scoped objects are valid only during a request.

Dangerous examples:

public class QuoteService {
    @Inject
    private SomeRequestScopedContext context;

    public void startAsyncWork() {
        executor.submit(() -> {
            context.getTenantId(); // may fail or use invalid context
        });
    }
}

Problems:

- request scope may be destroyed before async task runs
- proxy may throw context-not-active exception
- stale context may be used incorrectly

Safer approach:

String tenantId = context.getTenantId();
String actorId = context.getActorId();
String correlationId = context.getCorrelationId();

executor.submit(() -> asyncProcessor.process(new AsyncContext(
    tenantId,
    actorId,
    correlationId
)));

Capture value objects, not request-scoped framework objects.


14. Cancellation and Context Cleanup

Context cleanup must happen even when:

- resource method throws exception
- client disconnects
- timeout fires
- thread is interrupted
- downstream dependency fails
- async task is rejected

Pattern:

try {
    setContext();
    return proceed();
} catch (Exception e) {
    recordFailure(e);
    throw e;
} finally {
    clearContext();
}

For async tasks:

try {
    restoreContext();
    task.run();
} finally {
    clearContext();
}

If context cleanup is not deterministic, debugging becomes nondeterministic.


15. Failure Modes

Failure modeSymptomLikely cause
Missing correlation IDlogs cannot be tied togetherno inbound generation or async loss
Wrong tenant IDcross-tenant data riskstale ThreadLocal or untrusted header
Missing trace spanbroken distributed tracecontext not propagated across executor/client
Wrong actor in auditcompliance issueuser context reused or not captured
Random test failurescontext leak between testsThreadLocal/MDC not cleared
Memory leakthread local retains large objectThreadLocal value not removed
Security bypassprincipal missing/fallbackoptional security context mishandled
Log contaminationrequest B logs request A IDreused thread with stale MDC
Async NPErequest scope inactiverequest-scoped proxy used later

16. Debugging Workflow

When context propagation fails, debug by tracing the boundary.

1. Identify the missing/wrong context field.
2. Find where it should be created.
3. Find where it should be attached to current execution.
4. Find every boundary where execution changes thread/protocol.
5. Verify capture/restore/clear behavior.
6. Check logs before and after each boundary.
7. Check tests for context leak across test cases.

Useful log probes:

- thread name
- request ID
- correlation ID
- trace ID
- tenant ID
- actor ID
- operation name
- async task name

Temporary debugging log should not leak secrets or PII.


17. Testing Context Propagation

Minimum test cases:

[ ] inbound request without correlation ID generates one
[ ] inbound request with valid correlation ID preserves it
[ ] response includes correlation ID
[ ] error response includes correlation ID
[ ] outbound HTTP call propagates approved headers
[ ] async task preserves MDC/trace if required
[ ] ThreadLocal/MDC cleared after request
[ ] two sequential requests on same thread do not leak context
[ ] Kafka event includes required context headers
[ ] Kafka consumer creates correct processing context

Test for leak explicitly:

@Test
void clearsContextAfterFailure() {
    assertThrows(RuntimeException.class, () -> invokeFailingRequest());
    assertNull(MDC.get("correlation_id"));
}

Better integration test:

Send request A with correlation_id=A.
Send request B with correlation_id=B.
Assert logs/outbound calls/events for B never contain A.

18. PR Review Checklist

[ ] Does this code introduce a new async boundary?
[ ] Is the executor explicit and bounded?
[ ] Does context propagation happen deliberately?
[ ] Are MDC and ThreadLocal cleared in finally block?
[ ] Is tenant context explicit for business-critical operations?
[ ] Is SecurityContext used only within valid request lifecycle?
[ ] Are user/service identities modeled correctly?
[ ] Are correlation/causation IDs preserved across outbound calls?
[ ] Are Kafka headers populated/consumed consistently?
[ ] Are context propagation tests included?
[ ] Does code avoid passing JAX-RS framework context into domain logic?
[ ] Does logging include enough context without leaking PII?

19. Internal Verification Checklist

For CSG Quote & Order or any enterprise JAX-RS codebase, verify from actual evidence:

Runtime and framework
[ ] Is the service running on Jersey, another JAX-RS implementation, Servlet container, or embedded server?
[ ] Which request filters create correlation/request/tenant context?
[ ] Which response filters clear context?
[ ] Are filters ordered explicitly?

Logging
[ ] Which logging framework is used?
[ ] Is MDC used?
[ ] Which MDC keys are standard?
[ ] Is MDC cleared on success and error paths?
[ ] Are logs structured JSON or plain text?

Tracing
[ ] Is OpenTelemetry Java agent used?
[ ] Is manual instrumentation used?
[ ] Are executor/Kafka/HTTP client instrumentations enabled?
[ ] How are trace ID and correlation ID related?

Security and tenancy
[ ] Where is tenant resolved?
[ ] Is tenant taken from token claim, header, path, config, or gateway?
[ ] Is tenant validated against identity/permission?
[ ] Is tenant passed explicitly to repository queries?
[ ] Are audit events tenant-aware?

Async and executors
[ ] Which executors exist?
[ ] Are they bounded?
[ ] Are they context-aware?
[ ] Do they propagate MDC/OTel/security/tenant context?
[ ] Do they clear context after task completion?

Kafka/events
[ ] Which context headers are required on events?
[ ] Are correlation and causation IDs populated?
[ ] Is tenant ID included in event header or payload?
[ ] Is event processing context reconstructed on consume?

Tests
[ ] Are there tests for context propagation?
[ ] Are there tests for context cleanup?
[ ] Are there tests for cross-tenant leakage prevention?

20. Senior-Level Heuristics

If a value affects correctness, pass it explicitly.
If a value affects observability, propagate it consistently.
If a value affects security, validate it at the boundary.
If a value is stored in ThreadLocal, clear it deterministically.
If execution changes thread, assume context is lost until proven otherwise.

Context propagation should be boring. If every team invents its own propagation behavior, debugging production becomes archaeology.


21. Common Anti-Patterns

Anti-pattern: storing full request object in ThreadLocal
Risk: memory leak, lifecycle confusion, hidden dependency

Anti-pattern: domain logic reads tenant from static context
Risk: impossible-to-test tenant behavior and hidden cross-tenant bugs

Anti-pattern: async task uses request-scoped bean
Risk: context inactive or stale context

Anti-pattern: blindly forwarding all inbound headers
Risk: spoofed identity, tenant injection, data leak

Anti-pattern: using correlation ID as authorization proof
Risk: correlation is not identity

Anti-pattern: one log line at start and no context after async boundary
Risk: traceability disappears during actual work

22. Minimal Production Standard

A production-ready JAX-RS service should have:

[ ] standard inbound context creation
[ ] standard outbound context propagation
[ ] deterministic MDC/ThreadLocal cleanup
[ ] explicit tenant/security model
[ ] context-aware executor standard
[ ] Kafka/event context header standard
[ ] OpenTelemetry context propagation verified
[ ] tests for context propagation and cleanup
[ ] documented header/context contract
[ ] PR checklist for async boundary changes

23. Key Takeaways

- Request context is part of the runtime contract.
- ThreadLocal is powerful but dangerous in pooled-thread environments.
- MDC helps logs, but it must be propagated and cleared.
- SecurityContext is request-lifecycle-bound; do not treat it as durable identity.
- Tenant context must be explicit where correctness depends on it.
- Async boundary is a context boundary.
- HTTP and Kafka context propagation require governance.
- Broken context propagation creates silent production failures.

The next part builds on this by examining timeout, cancellation, and backpressure: the mechanisms that prevent slow or abandoned work from consuming the whole system.

Lesson Recap

You just completed lesson 33 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.