Project Reactor
Project Reactor
The reactive programming library for the JVM that powers Spring WebFlux and Spring Cloud Gateway, providing non-blocking, backpressure-aware streams via
Mono(0–1 element) andFlux(0–N elements).
Core Idea
Project Reactor is the implementation of the Reactive Streams specification used throughout the Spring ecosystem. It models asynchronous sequences as lazy, composable pipelines. Nothing executes until a subscriber subscribes. This lazy, non-blocking model is what enables Spring-Cloud-Gateway and Spring WebFlux to handle thousands of concurrent requests on a small, fixed thread pool.
Reactor is the engine behind the BFF-Pattern aggregation capability: Mono.zip() lets a BFF call N downstream services simultaneously and merge their results without blocking a thread.
Key Principles
- Nothing runs until you subscribe — a
MonoorFluxis a description of a computation, not an execution. Subscription triggers execution. - Non-blocking by design — operators transform publishers without allocating threads; I/O operations use async callbacks under the hood.
- Backpressure propagated — downstream subscribers signal demand upstream; producers respect it. This prevents buffer overflow under load.
How It Works
The Two Core Types
| Type | Cardinality | Analogy | BFF Use Case |
|---|---|---|---|
Mono<T> | 0 or 1 item | Optional / Future | Single service call, aggregated response |
Flux<T> | 0 to N items | Stream / Iterable | List of items, SSE stream |
Both are:
- Cold by default (execution restarts per subscriber)
- Can be made hot with
.share()or.cache() - Terminal: they complete, error, or cancel
Essential Operators for BFF Development
Creation
Mono.just(value) // wrap a known value
Mono.empty() // empty Mono (no item, just completion)
Mono.error(new RuntimeException("failed")) // immediate error
Mono.fromCallable(() -> blockingCall()) // wrap a blocking call
Flux.fromIterable(list) // create from a collectionTransformation
mono.map(item -> transform(item)) // synchronous transform
mono.flatMap(item -> anotherMono(item)) // async transform (subscribes to inner Mono)
flux.flatMap(item -> callService(item)) // async, concurrent by default
flux.concatMap(item -> callService(item)) // async, sequential (preserves order)Aggregation (most important for BFF)
// Parallel calls — subscribe to ALL simultaneously, emit when ALL complete
Mono.zip(userMono, ordersMono)
.map(tuple -> new DashboardResponse(tuple.getT1(), tuple.getT2()));
// Up to 8 arguments: Mono.zip(m1, m2, m3, ..., combinator)
Mono.zip(
userMono,
ordersMono,
inventoryMono,
(user, orders, inventory) -> new FullDashboard(user, orders, inventory)
);
// zipWith: inline variant for two Monos
userMono.zipWith(ordersMono, (user, orders) -> new Dashboard(user, orders));Error Handling
mono.onErrorResume(ex -> fallbackMono) // replace error with fallback publisher
mono.onErrorReturn(defaultValue) // replace error with a plain value
mono.onErrorMap(ex -> new CustomException(ex)) // translate exception type
mono.doOnError(ex -> log.error("...", ex)) // side effect on error (does not handle)Fallbacks and Defaults
mono.switchIfEmpty(Mono.just(defaultValue)) // fallback if upstream empty
mono.defaultIfEmpty(defaultValue) // simpler version of aboveTimeouts and Retry
mono.timeout(Duration.ofSeconds(3)) // error if no item within 3s
mono.retry(3) // retry up to 3 times on error
mono.retryWhen(Retry.backoff(3, Duration.ofMillis(100))) // exponential backoffCollecting Flux into Mono
flux.collectList() // Flux<T> → Mono<List<T>>
flux.count() // Flux<T> → Mono<Long>
flux.reduce(seed, combiner) // Flux<T> → Mono<R> via foldScheduling and Blocking Interop
// Offload blocking work to bounded elastic thread pool (never block event loop!)
Mono.fromCallable(() -> legacyBlockingService.call())
.subscribeOn(Schedulers.boundedElastic());Side Effects (logging, metrics)
mono.doOnSubscribe(sub -> log.debug("subscribing"))
.doOnNext(item -> log.debug("received: {}", item))
.doOnError(ex -> metrics.increment("errors"))
.doOnSuccess(item -> log.debug("completed"))
.doFinally(signalType -> log.debug("signal: {}", signalType));Marble Diagram Mental Model
Mono.just(A) : --A|
Mono.empty() : ---|
Mono.error(E) : --X(E)
Flux.just(A,B,C) : --A--B--C--|
Flux.error(E) : --X(E)
zip(A|, B|) → (A,B)| (completes when BOTH complete)
The Aggregation Pattern in Full
public Mono<DashboardResponse> buildDashboard(String userId) {
Mono<UserProfile> user = userClient.get(userId)
.timeout(Duration.ofSeconds(3))
.onErrorResume(e -> Mono.just(UserProfile.empty(userId)));
Mono<List<Order>> orders = orderClient.getRecent(userId)
.collectList()
.timeout(Duration.ofSeconds(3))
.onErrorResume(e -> Mono.just(List.of()));
Mono<AccountSummary> account = accountClient.getSummary(userId)
.timeout(Duration.ofSeconds(3))
.onErrorResume(e -> Mono.just(AccountSummary.empty()));
// All three subscribed simultaneously. Latency = max(t1, t2, t3).
return Mono.zip(user, orders, account)
.map(t -> new DashboardResponse(t.getT1(), t.getT2(), t.getT3()));
}Examples
- Simple transform:
Mono.just("hello").map(String::toUpperCase)→Mono.just("HELLO") - Parallel calls:
Mono.zip(callA(), callB()).map(t -> merge(t.getT1(), t.getT2())) - Error fallback:
callService().onErrorResume(e -> Mono.just(cachedValue)) - Collect list:
flux.collectList()converts a streaming response to aMono<List<T>>for zipping
Common Misconceptions
: It subscribes to all inputs simultaneously. Execution is parallel at the I/O level.Mono.zipis sequentialYou can block inside a reactive chain: Never call.block()inside a reactive pipeline or on an event-loop thread. UseSchedulers.boundedElastic()+subscribeOnfor any blocking call.:flatMapis always concurrentflatMaponFluxis concurrent by default.concatMapis sequential. Choose based on ordering requirements.An error in one: It fails the entire zip. UseMono.zipbranch fails silentlyonErrorResumeon each branch individually before zipping to implement partial failure / graceful degradation.
Why It Matters
For a BFF-Pattern implementation, Project Reactor is the abstraction that makes fan-out aggregation efficient:
- Without Reactor: aggregating N services requires N threads blocked on HTTP responses.
- With Reactor:
Mono.zip(...)drives N simultaneous HTTP calls throughWebClientwith zero extra threads. Total latency = slowest service, not sum of all services.
This is the core performance argument for choosing the reactive stack.
Related Concepts
| Concept | Relationship |
|---|---|
| Reactive-Programming | Reactor is the Spring implementation of reactive programming |
| Spring-Cloud-Gateway | SCG uses Reactor for all filter chain execution |
| BFF-Pattern | Reactor enables BFF aggregation patterns |
| Request-Aggregation | Implemented via Mono.zip |
Sources
- projectreactor.io/docs/core/release/reference (official Reactor Core reference)
- Reactor Core 3.6.x API Javadoc
- Spring WebFlux reference: docs.spring.io/spring-framework/reference/web/webflux.html
- P2-Spring-Boot-BFF-Stack (Phase 2 research)