How do you write to and read from the Reactor Context, and what is the difference between Context and ContextView?
answer
- ContextView = read-only (get/getOrEmpty)
- Context = writable (put/delete), immutable copies
- deferContextual gives ContextView
- get throws, getOrEmpty/Default safe
- subscriberContext() deprecated
basics
~10 sWrite 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.
solid answer
~40 sYou mutate the context via contextWrite(Function<Context,Context>) or contextWrite(Context.of(...)); each call produces a new immutable Context and applies to operators upstream of the write. To read, use Mono.deferContextual(ctxView -> ...) or Flux.deferContextual(...), which invokes your lambda at subscription time with a ContextView. ContextView is the read-only interface (get, getOrEmpty, getOrDefault, hasKey, size) — it's what you get when reading. Context extends the concept with put/putAll/delete used when writing. Reads: get(key) throws NoSuchElementException if missing, so prefer getOrEmpty (returns Optional) or getOrDefault. There is also transformDeferredContextual and the contextCapture() operator (Micrometer bridge). Keys are objects — commonly a constant String or Class token — and equality is by the key object.
code
java · 11 linesMono<String> lookup() {
return Mono.deferContextual(view -> {
String tenant = view.getOrDefault("tenant", "public"); // safe read
return callDownstream(tenant);
});
}
Mono<String> caller() {
return lookup()
.contextWrite(ctx -> ctx.put("tenant", "acme")); // upstream write
}go deeper
May only know contextWrite/deferContextual names.
Should distinguish Context vs ContextView and safe reads.
Should mention deprecated subscriberContext and key conventions (Class tokens).
Should mention contextCapture()/Micrometer bridge and design of key namespacing to avoid collisions.
## Two types: Context vs ContextView Reactor splits the abstraction in two: - **`ContextView`** — the **read-only** interface. Methods: `get(key)`, `getOrEmpty(key)` → `Optional`, `getOrDefault(key, default)`, `hasKey(key)`, `size()`, `isEmpty()`, `stream()`. This is what your read lambdas receive. - **`Context`** — extends the *idea* of a writable store. It adds `put(key, value)`, `putAll(...)`, `delete(key)`, `putNonNull(...)`. Every one of these returns a **new** `Context` (immutable / persistent data structure). `Context` also *is a* `ContextView` (you can read from it). The split exists so operators that only need to read can't accidentally mutate. ## Writing ```java .contextWrite(ctx -> ctx.put("tenant", "acme")) // or .contextWrite(Context.of("tenant", "acme", "traceId", id)) ``` `contextWrite` takes either a `Function<Context, Context>` (you receive the incoming context and return a modified copy) or a static `ContextView`/`Context` to merge in. Remember: it applies **upstream**. ## Reading The canonical read operators: - **`Mono.deferContextual(ctxView -> Mono<T>)`** and **`Flux.deferContextual(ctxView -> Publisher<T>)`** — defer creation of the publisher until subscription, giving you the `ContextView`. - **`someMono.transformDeferredContextual((mono, ctxView) -> ...)`** — transform an existing publisher with access to context. - Some operators have contextual overloads, e.g. `handle`, `doOnEach` (where `Signal` exposes `getContextView()`). Within these you read with `getOrDefault`/`getOrEmpty` to avoid the `NoSuchElementException` that bare `get` throws on a missing key. ## Keys Keys are arbitrary objects compared by `equals`. Conventions: a `public static final String` constant, or a `Class<?>` token (e.g. `context.get(TraceId.class)`) which also gives type-safe casting on read. ## Deprecated predecessors Older Reactor used `subscriberContext()` / `Mono.subscriberContext()`; these are deprecated in favor of `contextWrite` / `deferContextual`. Mention this if asked about legacy code. ## contextCapture() `contextCapture()` (Reactor 3.5+, needs Micrometer context-propagation on the classpath) automatically captures registered ThreadLocal values into the Context at that point — a bridge, not a manual write. ## Gotcha: order matters Because `contextWrite` affects upstream, a read via `deferContextual` sees writes placed **below** it in the chain but not writes placed **above** it. Getting this backwards is the #1 mistake.
- What happens if you call ctxView.get(key) for a missing key?It throws NoSuchElementException. Use getOrEmpty (Optional) or getOrDefault to read safely.
- Why does contextWrite return a new object each time?Context is immutable/persistent. Immutability makes it safe to share across threads without synchronization; each write yields a new Context leaving the original untouched.
saying these in an interview costs you the question
- Claiming you can mutate a Context in place via put
- Using bare get and being surprised by NoSuchElementException
- Thinking deferContextual sees writes placed above it in the chain