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) and Flux (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

  1. Nothing runs until you subscribe — a Mono or Flux is a description of a computation, not an execution. Subscription triggers execution.
  2. Non-blocking by design — operators transform publishers without allocating threads; I/O operations use async callbacks under the hood.
  3. Backpressure propagated — downstream subscribers signal demand upstream; producers respect it. This prevents buffer overflow under load.

How It Works

The Two Core Types

TypeCardinalityAnalogyBFF Use Case
Mono<T>0 or 1 itemOptional / FutureSingle service call, aggregated response
Flux<T>0 to N itemsStream / IterableList 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 collection

Transformation

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 above

Timeouts 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 backoff

Collecting 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 fold

Scheduling 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 a Mono<List<T>> for zipping

Common Misconceptions

  • Mono.zip is sequential: It subscribes to all inputs simultaneously. Execution is parallel at the I/O level.
  • You can block inside a reactive chain: Never call .block() inside a reactive pipeline or on an event-loop thread. Use Schedulers.boundedElastic() + subscribeOn for any blocking call.
  • flatMap is always concurrent: flatMap on Flux is concurrent by default. concatMap is sequential. Choose based on ordering requirements.
  • An error in one Mono.zip branch fails silently: It fails the entire zip. Use onErrorResume on 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 through WebClient with zero extra threads. Total latency = slowest service, not sum of all services.

This is the core performance argument for choosing the reactive stack.


ConceptRelationship
Reactive-ProgrammingReactor is the Spring implementation of reactive programming
Spring-Cloud-GatewaySCG uses Reactor for all filter chain execution
BFF-PatternReactor enables BFF aggregation patterns
Request-AggregationImplemented 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)