Explain precisely why the ThreadLocal-based SecurityContextHolder breaks on WebFlux's event loop, and what replaces it.
answer
- thread-per-request vs event-loop pool
- one thread interleaves many requests
- thread hops at flatMap/publishOn
- Reactor Context = subscription-scoped, immutable
- contextWrite up, deferContextual reads
basics
~20 sWebFlux 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.
solid answer
~40 sSecurityContextHolder's default strategy stores the SecurityContext in a ThreadLocal, which assumes a thread-per-request model — true on the Servlet stack. WebFlux instead runs on a small fixed pool of non-blocking event-loop threads. Two things break the ThreadLocal assumption: (1) a single event-loop thread interleaves many concurrent requests, so a per-thread slot can't represent one request's user; (2) a request's processing hops between threads as operators run on different schedulers, so even within one request the value set on thread A is invisible on thread B. Spring Security therefore stores the SecurityContext in the Reactor Context — an immutable key/value map bound to the subscription (the request's reactive chain), propagated implicitly by the framework as operators subscribe. You read it via ReactiveSecurityContextHolder.getContext(), and @PreAuthorize / @AuthenticationPrincipal read the same context.
code
java · 11 lines// Servlet model assumption (works there, NOT on WebFlux):
// SecurityContextHolder.getContext().getAuthentication(); // ThreadLocal
// Reactive: value lives in the subscription's Reactor Context
Mono<String> user = ReactiveSecurityContextHolder.getContext()
.map(ctx -> ctx.getAuthentication().getName());
// You can even seed it manually (e.g. tests) via contextWrite:
Mono<String> seeded = user.contextWrite(
ReactiveSecurityContextHolder.withAuthentication(
new TestingAuthenticationToken("alice", "pw", "ROLE_USER")));go deeper
Know the one-liner: ThreadLocal is per-thread, WebFlux shares threads across requests, so use ReactiveSecurityContextHolder.
Articulate both failure modes (interleaving + thread hopping) and name the Reactor Context as the replacement.
Explain subscription-scoping, immutability, contextWrite/deferContextual direction, and that method security reads the same context.
Reason about the design trade-off, blocking-library bridging pitfalls, and how context propagation composes across operator boundaries.
## Thread-per-request vs event loop Spring MVC on the Servlet stack dedicates **one thread to a request** from the moment the container accepts it until the response is written. Because the thread == the request, storing per-request state in a **`ThreadLocal`** is safe and cheap. That is exactly what `SecurityContextHolder` does with `SecurityContextHolderStrategy` (default `ThreadLocalSecurityContextHolderStrategy`). `SecurityContextPersistenceFilter`/`SecurityContextHolderFilter` sets it at the start and clears it in a `finally` at the end. WebFlux uses the **reactive / event-loop model** (Reactor Netty by default). There is a small, fixed set of **event-loop threads** — usually one per CPU core. These threads must never block; instead each unit of work is small and non-blocking, and threads are reused across all in-flight requests. ## Why the ThreadLocal fails — two independent reasons 1. **Interleaving (sharing one thread across requests):** one event-loop thread processes fragments of request R1, then R2, then R1 again. A ThreadLocal has exactly one slot per thread, so it cannot simultaneously hold R1's and R2's user. Whatever was last written wins, so you'd read the wrong principal. 2. **Thread hopping (one request across threads):** a reactive pipeline can switch threads at operators like `flatMap`, `publishOn`, `subscribeOn`, or when an async I/O callback resumes on a different event-loop thread. A value written to the ThreadLocal on thread A is simply not present on thread B. Even a single, isolated request would lose the context mid-chain. Either reason alone is fatal; together they make ThreadLocal unusable for security state on WebFlux. ## What replaces it: the Reactor Context Project Reactor provides **`Context`** (accessed via `ContextView`) — an **immutable** key/value map that is **attached to the subscription**, i.e. to the reactive chain for one request, *not* to any thread. Its properties: - It travels with the data flow regardless of which thread executes an operator, so thread hopping is a non-issue. - Each subscription has its own context, so interleaving on a shared thread is a non-issue. - It is immutable and flows **from the subscriber upstream toward the source** (written with `contextWrite`, formerly `subscriberContext`), then is visible to operators reading it with `Mono.deferContextual` / `Flux.deferContextual`. Spring Security stores the `SecurityContext` under a well-known key in the Reactor Context. `ReactiveSecurityContextHolder.getContext()` is essentially a `Mono.deferContextual` that looks up that key. Method security (`ReactiveMethodSecurity`, `@PreAuthorize`) and `@AuthenticationPrincipal` read the same context, so authorization works consistently. ## The mental model / gotcha - 'Per-thread' (ThreadLocal) → replaced by 'per-subscription' (Reactor Context). - You cannot smuggle the value across an unrelated `subscribe()` — the context only exists inside the connected chain you return from the handler. - Mixing blocking libraries that read `SecurityContextHolder` into a WebFlux app is a real trap: they'll see an empty ThreadLocal. Bridging requires the Micrometer context-propagation library (covered separately). ## When it matters in interviews This question separates candidates who 'use WebFlux' from those who understand *why* the reactive stack needed its own security propagation mechanism. The crisp answer is: ThreadLocal assumes thread==request; the event loop breaks that assumption in two ways, so state moves to the subscription-scoped Reactor Context.
- Name two operators that can cause a request to change threads mid-pipeline.publishOn (switches the thread for downstream operators) and subscribeOn (switches the thread the source subscribes on). flatMap can also resume on a different thread when its inner publisher completes on another scheduler. Any of these invalidate a ThreadLocal set earlier.
- Is the Reactor Context mutable? How is a value added?No — it's immutable. You add/override keys with contextWrite (older name subscriberContext), which returns a new context for the upstream chain. Reads use Mono.deferContextual/Flux.deferContextual, and it flows from subscriber toward the source.
saying these in an interview costs you the question
- Claiming WebFlux uses a thread pool sized per request like Tomcat, so ThreadLocal is fine.
- Saying you can call SecurityContextHolder.getContext() as long as you don't use flatMap.
- Believing the Reactor Context flows top-down from source to subscriber (it's the opposite for writes).
- Thinking the context is stored on a thread and copied between threads automatically without any mechanism.