ThreadLocal stores a value for each thread, not for each logical request. A worker reused for another request can retain stale context unless a scoped operation restores or removes it.
Java ThreadLocal: restore request context when a pooled thread is reused
Operational contract
The helper saves the current tenant, sets the new tenant for one synchronous Runnable, then restores the old value in finally. If there was no previous value it removes the entry, which prevents a pooled worker from carrying a tenant into the next task. Restoring rather than blindly removing also supports nested scopes on the same thread. This mechanism does not propagate to an asynchronous task automatically; pass identity explicitly or use a context mechanism whose lifetime matches the task. The tenant string here is diagnostic context, not authorization by itself.
Failure case
A worker processes depot-47 and then depot-82 on the same pool thread. A failure during the first task still executes finally. The second task cannot inherit depot-47 from this helper, and a nested call returns to its outer tenant after the inner call completes.
Java code
public class TenantTraceScope {
private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>();
public static void run(String tenantId, Runnable operation) {
String previous = CURRENT_TENANT.get();
CURRENT_TENANT.set(tenantId);
try {
operation.run();
} finally {
if (previous == null) CURRENT_TENANT.remove();
else CURRENT_TENANT.set(previous);
}
}
public static String current() {
return CURRENT_TENANT.get();
}
}Performance and ownership cost
get, set, and remove are expected O(1) per thread-local operation with per-thread storage. Each live worker can retain its current value until cleared or the worker ends. The finally block runs on ordinary exceptions; a process termination is outside that guarantee.
Common Mistakes
- Do not treat a thread-local tenant as a security check.
- Do not assume a child or executor task inherits the caller's context.
- Do not remove unconditionally when nested scopes must restore a previous value.
Connected lessons
- Java 25 scoped values: request context with a bounded lifetime
- Java 21 virtual threads: blocking tasks still need limits
- Java exceptions: recovery boundaries and failure context
- Java LongAccumulator: track a concurrent high-water mark
- Java LockSupport: park only inside a checked condition loop
- Java concurrent state and handoff quiz
- Advanced Java
