Explain why Mono.just(Instant.now()) gives every subscriber the same value while Mono.defer(() -> Mono.just(Instant.now())) does not. What does this reveal about assembly time vs subscription time?
answer
- assembly time vs subscription time
- just captures once, defer runs per subscriber
- retry re-subscribes -> defer re-attempts, just replays stale
- now()/random/mutable read -> wrap in defer
- defer + cache = lazy compute once then share
basics
~20 sjust captures its value once when the pipeline is built (assembly time), so all subscribers share it. defer runs its supplier on each subscription, producing a fresh value per subscriber. It shows Reactor separates building the pipeline from executing it.
solid answer
~40 sThere are two distinct phases in a reactive pipeline. **Assembly time** is when you construct the operator chain — the moment `Mono.just(Instant.now())` executes, `Instant.now()` is evaluated and that fixed value is baked into the Mono. Every later subscription just replays that same captured instant. **Subscription time** is when a subscriber connects and data actually flows. `Mono.defer(() -> Mono.just(Instant.now()))` moves the `Instant.now()` call into a supplier that Reactor invokes **freshly per subscription**, so each subscriber sees the current time. This distinction matters whenever the source is time-dependent, reads mutable/external state, must be retried (each retry re-subscribes — defer gives a fresh attempt), or needs per-subscriber context. defer is the idiomatic way to make an otherwise-eager source lazy and re-evaluable. It also affects caching: a cold defer recomputes; sharing/caching (`.cache()`) changes that.
code
java · 17 linesimport reactor.core.publisher.Mono;
import java.time.Instant;
// Same value for all subscribers: Instant.now() evaluated once at assembly
Mono<Instant> eager = Mono.just(Instant.now());
eager.subscribe(System.out::println); // T0
eager.subscribe(System.out::println); // T0 (identical)
// Fresh value per subscription: supplier invoked at each subscribe
Mono<Instant> lazy = Mono.defer(() -> Mono.just(Instant.now()));
lazy.subscribe(System.out::println); // T1
lazy.subscribe(System.out::println); // T2 (later)
// Retry pitfall: eager call happens once, retry replays it
Mono<Data> broken = Mono.just(callServiceThatMayFail()); // already ran/failed eagerly
// Correct: defer so each retry re-invokes the call
Mono<Data> retriable = Mono.defer(() -> callServiceReactive()).retry(3);go deeper
May not know the distinction; enough to recognize just captures a value.
Should explain per-subscriber freshness and the just-vs-defer difference for time/random.
Articulates assembly vs subscription time precisely and the retry re-subscription implication.
Connects to cold/hot semantics, cache()/share(), context propagation, and designs APIs that avoid accidental eager side effects.
## Two timelines in every Reactor pipeline When you write a reactive chain, two separate things happen at two separate times: 1. **Assembly time** — when the Java statements that *build* the operators run. `Mono.just(x)`, `.map(...)`, `.filter(...)` all execute here to wire up the chain. Anything evaluated as a plain argument (like `Instant.now()` passed into `just`) happens **now**, exactly once. 2. **Subscription time** — when someone calls `subscribe()` (or WebFlux subscribes to write the response). Only now does data flow: `onSubscribe → onNext... → onComplete/onError`. Because `Mono`/`Flux` are **cold** (each subscription triggers the source anew) and **lazy** (nothing flows before subscribe), *where* a computation sits — inside a plain argument vs inside a deferred supplier — changes its behavior dramatically. ## The `just` case ```java Mono<Instant> m = Mono.just(Instant.now()); // (A) Instant.now() runs HERE, once m.subscribe(t -> log(t)); // prints T0 m.subscribe(t -> log(t)); // prints T0 again — same captured instant ``` `Instant.now()` is an ordinary method call evaluated **at line (A)**, at assembly. The resulting instant is a constant embedded in the Mono. Re-subscribing replays that same constant. `just` does **not** re-run anything per subscription; it only re-emits the already-captured value. ## The `defer` case ```java Mono<Instant> m = Mono.defer(() -> Mono.just(Instant.now())); // supplier stored, not run m.subscribe(t -> log(t)); // prints T1 (now) m.subscribe(t -> log(t)); // prints T2 (later now) ``` `defer(Supplier<Publisher>)` stores the supplier at assembly but **invokes it once per subscriber, at subscription time**. Each invocation runs `Instant.now()` afresh, so each subscriber gets the current time. `defer` effectively says "don't decide what the publisher is until someone actually subscribes." ## Why this matters in practice - **Time / random / counters**: any `now()`, `UUID.randomUUID()`, or mutable read must be inside `defer`/`fromCallable` to be fresh per subscription. - **Retries**: `.retry()` and `.repeat()` **re-subscribe** to the upstream. With `just`, a retry replays the same stale value; with `defer`, each retry re-runs the supplier — genuinely re-attempting the work. This is a classic bug: `Mono.just(callService()).retry(3)` retries nothing (the call already happened once, eagerly), whereas `Mono.defer(() -> callService()).retry(3)` actually retries. - **Per-subscriber context / config**: defer can read `Context`, feature flags, or request-scoped data at the moment of subscription. - **Guarding eager factories**: if a method returns an eagerly-built Mono, wrapping the *call* in `Mono.defer(() -> buildIt())` postpones the eager work to subscription. ## Interaction with hot/shared operators defer keeps the source **cold** (recompute per subscriber). If you *want* one shared value across subscribers, add `.cache()` (turns it hot: first subscription computes, others replay). So `Mono.defer(...).cache()` = compute lazily once, then share — a different semantic again. ## fromCallable is a cousin `Mono.fromCallable(() -> Instant.now())` behaves like the defer case for a single value: the lambda runs per subscription and captures exceptions as onError. Use `fromCallable` for one value-producing call; use `defer` when you must rebuild an entire publisher/pipeline lazily. ## Mental model > `just` = "here is a value I already have." `defer` = "here is a recipe; make a fresh one each time someone asks."
- How does defer interact with retry() and repeat()?Both retry() and repeat() re-subscribe to the upstream. defer's supplier fires again on every re-subscription, producing a genuinely fresh attempt/value each time. Without defer (e.g. an eagerly-captured value in just), the re-subscription just replays the same stale, already-computed result.
- You want lazy computation but a single shared result across all subscribers — how?Combine defer (or fromCallable) with .cache(): the first subscription triggers the computation, and .cache() turns the Mono hot so subsequent subscribers replay the cached value instead of recomputing.
saying these in an interview costs you the question
- Thinking just re-evaluates its argument on each subscription
- Believing Mono.just(callService()).retry(3) actually retries the service call
- Conflating assembly time with subscription time
- Assuming defer makes the source hot/shared (it stays cold unless you add cache/share)