What is the Reactor Context and why does reactive code need it instead of ThreadLocal?
answer
- ThreadLocal dies on thread hops
- bound to Subscriber not thread
- immutable key-value, propagates upstream
- contextWrite / deferContextual
- trace ID, tenant, security principal
basics
~10 sReactor Context is a small immutable key-value map carried along a reactive pipeline. It replaces ThreadLocal because reactive operators hop between threads, so ThreadLocal values would be lost.
solid answer
~40 sIn blocking code we store per-request data (trace IDs, tenant, security principal) in ThreadLocal because one thread handles the whole request. In WebFlux, a single request is processed by many operators that may run on different threads (event-loop, schedulers), so ThreadLocal set at the start is gone later. Reactor solves this with Context: an immutable, type-safe key-value store bound to the Subscriber, not the thread. You write to it with contextWrite() and read it with Mono.deferContextual / Flux.deferContextual (or contextual variants of operators). Because it travels with the subscription rather than the thread, the data survives every thread hop. It is immutable, so each write produces a new Context, and it propagates upstream (bottom to top), which is the opposite of data flow.
code
java · 6 linesMono<String> handler() {
return Mono.deferContextual(ctx ->
Mono.just("tenant=" + ctx.get("tenant")))
// written BELOW the read, but propagates upstream so the read sees it
.contextWrite(context -> context.put("tenant", "acme"));
}go deeper
Should know ThreadLocal breaks in reactive and Context replaces it; knowing the exact operators is a plus.
Should name contextWrite/deferContextual and the immutability property.
Should explain upstream propagation and cite ReactiveSecurityContextHolder.
Should discuss context-propagation bridging to ThreadLocal-based libraries and design trade-offs.
## The problem In a traditional blocking Spring MVC app, one HTTP request is handled start-to-finish by a single thread. This lets us stash request-scoped data — a correlation/trace ID, the tenant, the `SecurityContext` — in a **ThreadLocal**: any code running on that thread can read it without passing it as a parameter. In **Spring WebFlux / Project Reactor**, a request is processed as a chain of operators (`map`, `flatMap`, etc.) on a `Mono`/`Flux`. These operators can execute on **different threads** — the Netty event loop, a `boundedElastic` scheduler after `subscribeOn`/`publishOn`, etc. So a value put into a `ThreadLocal` when the request starts is **not visible** on the thread that runs a later operator. ThreadLocal is fundamentally incompatible with the thread-hopping, deferred nature of reactive pipelines. ## The solution: Reactor Context `reactor.util.context.Context` (and its read-only interface `ContextView`) is an **immutable, type-safe, key-value store** that is bound to the **Subscriber/subscription**, not to a thread. Because it rides along the subscription, it survives every thread hop. Key properties: - **Immutable**: every write returns a *new* Context; you never mutate in place. This makes it safe to share across threads. - **Keyed by object**: keys are arbitrary objects (often a `String` or a `Class<?>` used as a token). `context.get(key)` throws if the key is absent; `getOrDefault`/`getOrEmpty` are the safe reads. - **Propagates UPSTREAM**: this is the crucial mental model. Data flows downstream (source → subscriber), but Context assembly flows **upstream** (subscriber → source). `contextWrite(...)` affects operators **above** it in the chain, not below. ## Writing and reading - **Write**: `mono.contextWrite(ctx -> ctx.put(KEY, value))` or `contextWrite(Context.of(KEY, value))`. - **Read**: `Mono.deferContextual(ctxView -> ...)` / `Flux.deferContextual(...)`, or `Mono.transformDeferredContextual`. `deferContextual` gives you a `ContextView` at subscription time so you can build the actual publisher using context values. ## When to use - Cross-cutting request-scoped data that must survive thread hops: **trace/correlation IDs, tenant IDs, locale, and the reactive security principal** (`ReactiveSecurityContextHolder` is literally implemented on top of Context). - Anything you would have reached for a ThreadLocal for in blocking code. Don't use it as a general-purpose parameter-passing mechanism between your own operators — pass values explicitly where you can. Reserve Context for genuinely cross-cutting concerns. ## Gotchas - Because propagation is upstream, `contextWrite` placed at the very end of a chain still supplies values to the beginning — but a `contextWrite` placed *below* a `deferContextual` will NOT be seen by that `deferContextual` (it's downstream of the write). - Reading a missing key with `get` throws `NoSuchElementException`. - Blocking libraries that read ThreadLocal (MDC, old SecurityContextHolder) don't automatically see Context — you need bridging (e.g. the Micrometer **context-propagation** library / `ContextRegistry`) to copy Context into ThreadLocal around a blocking hop.
- Where is the Context stored if not on the thread?On the Subscriber/subscription. It is assembled during subscription and travels with the subscription, so it is available regardless of which thread runs an operator.
- Name a real Spring feature built on Reactor Context.Reactive Spring Security: ReactiveSecurityContextHolder stores/retrieves the Authentication in the Reactor Context instead of a ThreadLocal SecurityContextHolder.
saying these in an interview costs you the question
- Saying ThreadLocal works fine in WebFlux as long as you set it early
- Thinking Context is mutable / stored on a thread
- Confusing Context with request attributes on ServerWebExchange