Walk through the main Reactor factory methods (just, empty, error, fromCallable, defer, fromIterable) and when you'd reach for each.
answer
- just = eager, evaluated at assembly
- fromCallable = lazy single call, throws -> onError
- defer = rebuild publisher per subscriber
- empty = complete-only, error = onError
- fromIterable = collection -> Flux
basics
~20 sjust wraps a ready value; empty completes with nothing; error emits a failure; fromCallable defers a synchronous/blocking call and captures its exceptions; defer builds a fresh publisher lazily per subscription; fromIterable turns a collection into a Flux.
solid answer
~40 sThese are the standard ways to create Mono/Flux. `just(value)` wraps an already-computed value — the value is evaluated eagerly at assembly time. `empty()` produces a publisher that just completes (no items). `error(ex)` produces one that terminates with the given throwable. `fromCallable(() -> ...)` wraps a synchronous, possibly-blocking or exception-throwing computation, deferring it to subscription and routing thrown exceptions into onError instead of blowing up assembly. `defer(() -> publisher)` postpones building the *whole* publisher until each subscription, giving per-subscriber freshness (new timestamps, re-reading mutable state, capturing subscriber context). `Flux.fromIterable(collection)` streams an existing List/Set. The key axis is eager (just) vs lazy (fromCallable/defer): use lazy whenever the value comes from a side effect, a blocking call, or must be recomputed per subscription.
code
java · 23 linesimport reactor.core.publisher.Mono;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;
import java.time.Instant;
import java.util.List;
// Eager: value computed once, right now
Mono<String> eager = Mono.just("ready");
// Complete with nothing / terminate with error
Mono<String> none = Mono.empty();
Mono<String> boom = Mono.error(new IllegalStateException("nope"));
// Lazy single call — blocking work offloaded, exceptions -> onError
Mono<byte[]> file = Mono.fromCallable(() -> readFileBlocking())
.subscribeOn(Schedulers.boundedElastic());
// defer: each subscriber gets a FRESH timestamp
Mono<Instant> freshNow = Mono.defer(() -> Mono.just(Instant.now()));
// vs Mono.just(Instant.now()) -> same instant for every subscriber
// Collection -> stream of elements
Flux<String> letters = Flux.fromIterable(List.of("a", "b", "c"));go deeper
Can name just/empty/error and roughly what each does.
Must articulate eager (just) vs lazy (fromCallable/defer) and know fromCallable routes exceptions into onError.
Explains per-subscriber freshness of defer, pairs fromCallable with subscribeOn(boundedElastic), knows the error() supplier overload.
Reasons about assembly-time vs subscription-time execution, context propagation via defer, and one-shot iterable/stream re-subscription hazards.
## Why factory methods matter A `Mono`/`Flux` is a **cold, lazy blueprint** — but *how* you feed a value into it decides *when* that value is computed. The factory methods differ precisely on **eager-vs-lazy** and **single-vs-many**. ### `just` — eager, ready value ```java Mono<String> m = Mono.just(compute()); // compute() runs NOW, at assembly Flux<Integer> f = Flux.just(1, 2, 3); ``` `just` takes a value that **already exists**. The argument expression is evaluated **immediately when the line runs**, *not* at subscription. So `Mono.just(callBlockingService())` executes the blocking call eagerly and captures its *result*; if it throws, the exception escapes right there (not as an onError signal). Rule of thumb: only pass a **constant or already-held** value to `just`. ### `empty` — complete with nothing ```java Mono<User> m = Mono.empty(); // 0 items, then onComplete Flux<User> f = Flux.empty(); ``` Used for "not found" (`switchIfEmpty` targets), no-op branches, or `Mono<Void>`-style completions. ### `error` — terminate with a throwable ```java Mono<User> m = Mono.error(new NotFoundException()); ``` Emits `onError` immediately on subscription. Note: `Mono.error(new Ex())` still **constructs** the exception eagerly at assembly; to defer even the exception creation use `Mono.error(() -> new Ex())` (the supplier overload) or wrap in `defer`. ### `fromCallable` — lazy single synchronous computation ```java Mono<Config> m = Mono.fromCallable(() -> readFileBlocking()); ``` Takes a `java.util.concurrent.Callable`. The lambda runs **at subscription time**, and any thrown checked/unchecked exception is **captured and turned into onError** instead of propagating at assembly. This is the correct wrapper for **blocking or exception-throwing synchronous code** — and you typically pair it with `.subscribeOn(Schedulers.boundedElastic())` to move the blocking work off the event-loop thread. (`Mono.fromSupplier` is the same idea but takes a `Supplier` and can't throw checked exceptions.) ### `defer` — lazy assembly of the whole publisher ```java Mono<Instant> now = Mono.defer(() -> Mono.just(Instant.now())); ``` `defer` takes a `Supplier<? extends Publisher>` and invokes it **fresh for every subscriber**. Contrast with `Mono.just(Instant.now())`, where `Instant.now()` is fixed once at assembly and every subscriber sees the *same* timestamp. Use `defer` when: the source must be rebuilt per subscription, you need per-subscriber state/context, you're wrapping a factory that itself decides eagerly, or you want to defer a whole conditional pipeline. `defer` is the general-purpose "make this lazy" tool; `fromCallable` is the narrower "defer one value-producing call." ### `fromIterable` — existing collection into a Flux ```java Flux<String> f = Flux.fromIterable(List.of("a", "b", "c")); ``` Streams each element of an existing `Iterable` as `onNext` signals. Related: `Flux.fromStream` (Java `Stream`, one-shot), `Flux.fromArray`. The iterable is iterated **at subscription**, so re-subscribing re-iterates it (careful with one-shot iterables). ## The eager-vs-lazy trap (most-tested gotcha) ```java // BUG: blocking call runs eagerly at assembly, on the calling thread Mono<Data> bad = Mono.just(repository.blockingLoad()); // FIX: deferred to subscription, exceptions -> onError, offloadable Mono<Data> good = Mono.fromCallable(() -> repository.blockingLoad()) .subscribeOn(Schedulers.boundedElastic()); ``` Because `just` evaluates its argument immediately, side-effecting or blocking code inside `just(...)` defeats the whole point of reactive laziness. Interviewers love this distinction. ## Quick decision table - Constant/held value → `just` - No value, just complete → `empty` - Known failure → `error` (supplier form to defer the throwable) - Blocking/throwing synchronous call, single value → `fromCallable` (+ `subscribeOn`) - Rebuild whole publisher per subscription / per-subscriber freshness → `defer` - Existing collection → `Flux.fromIterable`
- Why is Mono.just(blockingCall()) a bug, and what should you use instead?just evaluates its argument eagerly at assembly time, so the blocking call runs synchronously on the current (often event-loop) thread and any exception escapes outside the reactive signal path. Use Mono.fromCallable(() -> blockingCall()).subscribeOn(Schedulers.boundedElastic()) to defer it and offload the blocking work.
- What's the difference between defer and fromCallable?fromCallable defers a single value-producing (Callable) computation and wraps its result in a Mono. defer defers construction of an entire Publisher via a Supplier, invoked fresh per subscriber — more general, letting you rebuild an arbitrary pipeline (with new timestamps, re-read state, conditional branching) on each subscription.
saying these in an interview costs you the question
- Believing Mono.just(x) evaluates x lazily at subscription time
- Using just to wrap a blocking/IO call
- Thinking defer and just behave identically for time-varying or mutable sources
- Claiming fromCallable's thrown exception crashes assembly rather than becoming an onError signal
- Assuming Mono.error(new Ex()) constructs the exception lazily (it doesn't; use the supplier overload)