Explain why Reactor Context propagates upstream and how operator placement affects what a deferContextual read sees.
answer
- subscription flows upstream, signals downstream
- Context rides the subscription
- write affects operators above it
- read sees writes below it in the chain
- put contextWrite at the outer edge / WebFilter
basics
~10 sContext 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.
solid answer
~40 sData (onNext) flows downstream: source to subscriber. But subscription flows upstream: when you subscribe, each operator subscribes to its parent up to the source. Context is populated during that subscription phase, so contextWrite influences everything upstream of it — the operators that subscribe after it. Concretely, a deferContextual reads the Context that has been accumulated by all contextWrite calls positioned below (downstream of) it in the declaration order. A write placed above the read is downstream of it and therefore invisible. This inverts normal intuition. Practical rule: place contextWrite near the end/outer edge of the chain (or at the point where you have the value) so the upstream operators that need the data can see it. Nested contextWrites merge, with the closest-to-source (upper) write's keys potentially overridden depending on merge order.
code
java · 10 lines// WebFilter seeds Context for the whole downstream handler chain
@Component
class TenantContextFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String tenant = exchange.getRequest().getHeaders().getFirst("X-Tenant");
return chain.filter(exchange)
.contextWrite(ctx -> ctx.put("tenant", tenant == null ? "public" : tenant));
}
}go deeper
Unlikely to explain directionality.
Knows contextWrite exists but may get direction wrong.
Should articulate upstream subscription vs downstream signals and correct placement.
Should discuss WebFilter seeding, merge/override semantics, and why the source needs Context pre-assembled.
## Two directions of flow A reactive chain has two opposite flows: 1. **Subscription / assembly** flows **upstream**: `subscribe()` on the terminal operator triggers it to subscribe to its parent, which subscribes to *its* parent, all the way to the source. 2. **Signals** (`onNext`/`onError`/`onComplete`) flow **downstream**: source → subscriber. The **Context is threaded through the subscription flow**. When you call `contextWrite`, it wraps the subscription so that when it propagates the `subscribe` upward, the parent operators receive the augmented Context. Therefore a `contextWrite` modifies the Context seen by everything **upstream** (closer to the source) of that operator. ## What a read sees `Mono.deferContextual(view -> ...)` reads the Context at *its* position. It sees the cumulative effect of every `contextWrite` that sits **below it** (downstream, toward the subscriber) in the declared chain — because those writes are encountered first as subscription travels upward and enrich the Context before it reaches the read. A `contextWrite` placed **above** the `deferContextual` (upstream, toward the source) is invisible to that read, because subscription reaches the write *after* it has already passed the read. ### Illustration ```java Mono.deferContextual(v -> Mono.just(v.getOrDefault("k", "MISSING"))) // read .contextWrite(c -> c.put("k", "seen")) // BELOW read -> visible .map(x -> x); // emits "seen" Mono.just("x") .contextWrite(c -> c.put("k", "hidden")) // ABOVE read -> invisible .flatMap(x -> Mono.deferContextual(v -> Mono.just(v.getOrDefault("k", "MISSING")))) // the deferContextual here is upstream-unaware of a write placed above the source... // placement relative to the read is what matters ``` ## Merge semantics of nested writes When multiple `contextWrite`s stack, they merge. Each `contextWrite(Function)` receives the Context already assembled from downstream writes and returns a new one. If two writes use the same key, the write **closer to the subscriber** runs first (during upstream assembly) and the one **closer to the source** runs later and can therefore override it for operators above it — but operators *between* them see the earlier value. In practice, avoid overlapping keys across levels; namespace them. ## Practical guidance - Put `contextWrite` at the **outermost** edge (end of the chain, or in a `WebFilter`) so the whole pipeline upstream can read it. - In WebFlux, a `WebFilter` that does `chain.filter(exchange).contextWrite(...)` seeds Context for the entire downstream handler because the handler is upstream of the write in the composed chain. - When debugging "my value is missing," the first thing to check is whether the write is on the correct side of the read. ## Why this design Context must be known *before* the source emits (e.g. the source may need the tenant to pick a database). Since the source is subscribed last, populating Context during upstream subscription guarantees it's fully assembled by the time the source runs.
- If a value is missing at read time, what's the first thing to check?Whether the contextWrite is on the correct side of the read — it must be downstream (below) the deferContextual, otherwise the read is upstream of the write and won't see it.
- Why does a WebFilter's contextWrite reach the controller handler?The handler executes as part of the chain returned by chain.filter(exchange), which is upstream of the filter's contextWrite, so the whole handler pipeline sees the seeded Context.
saying these in an interview costs you the question
- Saying Context flows downstream like data
- Placing contextWrite before the read and expecting it to be visible
- Believing operator declaration order equals context visibility order (it's inverted)