skip to content

Reactor Context (contextWrite)

The Reactor Context is an immutable map attached to the subscription and written from downstream with contextWrite, replacing ThreadLocals for tracing and tenant data. Interviewers ask why it propagates upwards, which is the part people get wrong.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

5

What is the Reactor Context and why does reactive code need it instead of ThreadLocal?

level: juniorimportance: must knowfreq 70%

answer

  1. ThreadLocal dies on thread hops
  2. bound to Subscriber not thread
  3. immutable key-value, propagates upstream
  4. contextWrite / deferContextual
  5. trace ID, tenant, security principal

basics

~10 s

Reactor 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 s

In 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 lines
java
Mono<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

for a junior

Should know ThreadLocal breaks in reactive and Context replaces it; knowing the exact operators is a plus.

for a middle

Should name contextWrite/deferContextual and the immutability property.

for a senior

Should explain upstream propagation and cite ReactiveSecurityContextHolder.

for a principal

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

context

open as a page

How do you write to and read from the Reactor Context, and what is the difference between Context and ContextView?

level: middleimportance: must knowfreq 60%

basics

~10 s

Write with contextWrite() (returns a new immutable Context). Read with Mono/Flux.deferContextual, which hands you a ContextView. Context is the writable builder; ContextView is the read-only view operators receive.

open as a page

Explain why Reactor Context propagates upstream and how operator placement affects what a deferContextual read sees.

level: seniorimportance: should knowfreq 45%

basics

~10 s

Context is assembled during subscription, which travels from the subscriber up to the source. So contextWrite affects operators above it. A deferContextual sees writes placed below it in the chain, not above.

open as a page

How do you bridge Reactor Context with ThreadLocal-based libraries like SLF4J MDC for logging trace IDs?

level: principalimportance: should knowfreq 35%

basics

~20 s

Reactor Context isn't automatically visible to MDC (a ThreadLocal). Use the Micrometer context-propagation library with contextCapture()/automatic context propagation, or manually copy Context values into MDC around log points, to make trace IDs appear in logs.

open as a page

When would you use Reactor Context versus ServerWebExchange attributes to carry request-scoped data in WebFlux?

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

Use ServerWebExchange attributes for web-layer data available where you hold the exchange (filters, controllers). Use Reactor Context for data that must flow deep into the reactive pipeline and service layer where no exchange is passed, and for anything Reactor/Security reads from Context.

open as a page