skip to content

Reactive Context & Choosing WebFlux

Carrying context without ThreadLocals, propagating the security principal reactively, and deciding whether WebFlux is worth it at all. The last of those is the question most likely to decide a system-design conversation.

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

explore

questions

15

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

In a Spring WebFlux controller, how do you obtain the currently authenticated user, and why can't you just call SecurityContextHolder.getContext()?

level: juniorimportance: must knowfreq 70%

basics

~10 s

Use ReactiveSecurityContextHolder.getContext(), which returns a Mono<SecurityContext> you map to the Authentication/principal. The blocking SecurityContextHolder reads a ThreadLocal, which isn't reliably set on WebFlux's shared event-loop threads.

open as a page

What is the core difference between Spring MVC and Spring WebFlux, and when would you pick each?

level: juniorimportance: must knowfreq 75%

basics

~20 s

MVC is blocking and uses one thread per request; WebFlux is non-blocking and reactive, handling many concurrent requests on a few threads. Pick WebFlux for high concurrency with slow I/O; MVC for simpler, mostly CPU or blocking-DB work.

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 precisely why the ThreadLocal-based SecurityContextHolder breaks on WebFlux's event loop, and what replaces it.

level: middleimportance: must knowfreq 65%

basics

~20 s

WebFlux uses a few event-loop threads that serve many requests and hop threads between operators. A ThreadLocal is bound to a thread, not a request, so it's empty or holds the wrong user. The Reactor Context (subscription-scoped) replaces it.

open as a page

Can you mix reactive and imperative in one system? What are the legitimate ways vs the traps?

level: middleimportance: should knowfreq 45%

basics

~20 s

Yes at the system level: some services on WebFlux, others on MVC. Within one app you can't run both web stacks, but MVC can use WebClient, and WebFlux can bridge blocking calls via Schedulers.boundedElastic. The trap is a blocking call on the event loop.

open as a page

Give concrete scenarios where reactive WebFlux genuinely pays off — and one where teams adopt it but shouldn't.

level: middleimportance: should knowfreq 55%

basics

~20 s

Pays off for API gateways/fan-out over many slow downstreams, streaming (SSE/WebSocket) with backpressure, and huge concurrent-connection counts on limited memory. It backfires when the app is CPU-bound or built on blocking JDBC that can't be made reactive.

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

A developer reads the principal fine at the top of a WebFlux handler but gets an empty Mono deeper in the pipeline. What common mistakes break Reactor Context propagation of the security context?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The context is lost whenever you leave the connected chain: subscribing to a separate publisher, using an independent subscribe()/block, spawning work whose context you don't propagate, or mixing in blocking code that reads SecurityContextHolder. Keep everything in one returned chain.

open as a page

How does the authenticated principal actually get into the Reactor Context so ReactiveSecurityContextHolder can read it, and how would you seed it manually in a test?

level: seniorimportance: should knowfreq 45%

basics

~10 s

Spring Security's reactive filter chain authenticates the request and writes the SecurityContext into the Reactor Context with contextWrite. In tests you seed it via ReactiveSecurityContextHolder.withAuthentication(...) / .withSecurityContext(...) using contextWrite, or the @WithMockUser annotation.

open as a page

What are the hidden costs of going all-reactive, and how do you catch the classic mistakes?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Costs: harder debugging (broken stack traces, no ThreadLocal), a steep functional learning curve, less mature libraries, and one blocking call can stall the event loop. Catch mistakes with BlockHound, Reactor context/checkpoints, and load tests — not just unit tests.

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

As an architect, how do you decide WebFlux vs MVC for a new service in 2026 — and where do virtual threads change the calculus?

level: principalimportance: should knowfreq 40%

basics

~20 s

Default to MVC (now with virtual threads) for its simplicity. Choose WebFlux only when you truly need streaming/backpressure or extreme concurrent-connection scaling, and the whole chain is non-blocking. Virtual threads absorb most I/O-bound-but-not-streaming cases, shrinking WebFlux's niche.

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

You must call a blocking library that reads SecurityContextHolder (ThreadLocal) from within a WebFlux request. How do you bridge the reactive SecurityContext to that ThreadLocal correctly, and what are the risks?

level: principalimportance: nice to knowfreq 20%

basics

~10 s

Use the Micrometer context-propagation library to copy the reactive SecurityContext into a ThreadLocal for the blocking call. Enable Reactor's automatic context propagation (Hooks.enableAutomaticContextPropagation) or contextCapture(), and run the blocking code on Schedulers.boundedElastic().

open as a page