Mono vs Flux with real use cases, back-pressure in WebFlux, why Netty (and not a blocking servlet model), error handling with @ControllerAdvice, onErrorResume and onErrorMap, RouterFunction vs annotated controllers, reactive database access challenges and best practices, R2DBC vs JDBC, reactive transactions, WebTestClient, blocking code inside WebFlux, flatMap vs concatMap vs switchMap, Reactor Context vs ThreadLocal, and WebClient vs RestTemplate/RestClient performance.
Published September 25, 2026
Reactive interviews test mental models:
Pair every answer with the pragmatic view: for most CRUD services on Java 21+, MVC with virtual threads is simpler. WebFlux shines for streaming and high fan-out.
Mono and Flux? Give real use cases for both.Short answer: Both are Reactor Publishers (Reactive Streams):
Mono<T>: 0 or 1 element, then completion or an error. It's the async equivalent of Optional/CompletableFuture. Uses: fetch a user by ID, save an entity, call one downstream API, Mono<Void> for fire-and-complete operations.Flux<T>: 0..N elements (possibly infinite), then completion or an error. Uses: query results streamed row by row, server-sent events (live prices, notifications), Kafka message streams, WebSocket messages, paginated API aggregation, and file-line processing.@GetMapping("/orders/{id}")
Mono<OrderDto> one(@PathVariable UUID id) { return orders.findById(id).map(mapper::toDto)
.switchIfEmpty(Mono.error(new OrderNotFound(id))); }
@GetMapping(value = "/orders/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<OrderEvent> stream() { return events.asFlux(); } // pushed to the client as they occur
Short answer:
request(n) demand propagates upstream through the Reactor operators. Operators have bounded prefetch queues (limitRate, prefetch parameters), so a slow consumer slows the producers. For sources that can't slow down, you choose an overflow strategy: onBackpressureBuffer(max), Drop, Latest, or sampling.Flux.Short answer: WebFlux needs a non-blocking server. Reactor Netty (the default) is built on Netty's event loops: a few threads (about the number of cores) handle all the connections, driven by epoll or kqueue readiness events. That fits the reactive model perfectly, and supports HTTP/2, WebSockets, SSE and back-pressure natively.
Tomcat, Jetty or Undertow can run WebFlux through the Servlet 3.1+ non-blocking I/O API. It works, but you lose some efficiency and features compared with native Netty. Their traditional thread-per-request blocking model is what Spring MVC uses.
Key points to cover:
@ControllerAdvice, or onErrorResume()?Short answer: Both, at different levels:
@RestControllerAdvice with @ExceptionHandler works for annotated controllers in WebFlux too, returning ProblemDetail/ResponseEntity/Mono. For functional routes, use a WebExceptionHandler or ErrorWebExceptionHandler (Boot provides a default one).onErrorResume(fn): recover by switching to a fallback publisher (cached data, a default, or another source);onErrorReturn(value): a static fallback;onErrorMap(fn): translate the exception (for example WebClientResponseException.NotFound to a domain ProductNotFound), which is still an error;doOnError for logging, retryWhen(Retry.backoff(...)) for transient failures, and timeout(...).Mono<Price> price(String sku) {
return pricingClient.get(sku)
.timeout(Duration.ofMillis(500))
.retryWhen(Retry.backoff(2, Duration.ofMillis(100)).filter(ex -> ex instanceof WebClientRequestException))
.onErrorMap(WebClientResponseException.NotFound.class, ex -> new PriceUnavailable(sku))
.onErrorResume(TimeoutException.class, ex -> cache.lastKnown(sku)); // degraded, but served
}
Common trap: onErrorResume without a type predicate swallows every error, including bugs.
RouterFunction, and when would you use it instead of annotations?Short answer: Functional endpoints (WebFlux, and also Spring MVC since 5.2) define routes as code: RouterFunctions.route().GET("/orders/{id}", handler::get).POST(...).filter(...).build(), with handler functions ServerRequest → Mono<ServerResponse>. Use them when:
Annotated controllers are more familiar and declarative, and integrate easily with validation and OpenAPI tooling. Both can coexist.
Short answer:
Context, not in ThreadLocals.r2dbc-pool) still bound the concurrency.Mono.fromCallable(...).subscribeOn(Schedulers.boundedElastic()), with limits. Treat that as a smell.Flux), rather than collecting lists.Short answer:
Publishers (Postgres, MySQL, MariaDB, SQL Server, Oracle, H2), with back-pressure support, driven by the database protocol. It's not an ORM: Spring Data R2DBC offers simple entity mapping and repositories, but no lazy loading, relationships management or dirty checking.The implementation:
spring-boot-starter-data-r2dbc plus a driver.spring.r2dbc.url=r2dbc:postgresql://….ReactiveCrudRepository, R2dbcEntityTemplate, or DatabaseClient for SQL.interface OrderRepository extends ReactiveCrudRepository<OrderRow, UUID> {
Flux<OrderRow> findByCustomerIdOrderByCreatedAtDesc(UUID customerId);
}
Flux<OrderSummary> summaries = client.sql("SELECT id, total FROM orders WHERE status = :s")
.bind("s", "PAID").map((row, meta) -> new OrderSummary(row.get("id", UUID.class), row.get("total", BigDecimal.class)))
.all();
Short answer: In reactive code, a transaction can't be bound to a thread (ThreadLocal), because a pipeline hops between threads. Spring uses a ReactiveTransactionManager (R2dbcTransactionManager, ReactiveMongoTransactionManager), and stores the transaction state in the Reactor Context, tied to the subscription. Two ways to use it:
@Transactional on methods returning Mono/Flux: the transaction begins on subscription, and commits on completion, or rolls back on error or cancellation;TransactionalOperator: operator.transactional(publisher), or execute(...), programmatically.The caveats:
subscribe() inside) escapes it;WebTestClient?Short answer: WebTestClient is a non-blocking test client with a fluent assertion API:
@WebFluxTest(OrderController.class), plus a mocked service (@MockitoBean), for a slice test;@SpringBootTest(webEnvironment = RANDOM_PORT) for a full server test;WebTestClient.bindToRouterFunction(...)/bindToController(...).Test the pipelines themselves with StepVerifier (Reactor Test): expectNext, expectError, thenCancel, and virtual time for delays. (WebTestClient can also test Spring MVC applications.)
@WebFluxTest(OrderController.class)
class OrderControllerTest {
@Autowired WebTestClient client;
@MockitoBean OrderService service;
@Test void returnsOrder() {
given(service.get(ID)).willReturn(Mono.just(new OrderDto(ID, "PAID")));
client.get().uri("/orders/{id}", ID).exchange()
.expectStatus().isOk()
.expectBody().jsonPath("$.status").isEqualTo("PAID");
}
}
Short answer: Technically yes, but carefully. Blocking on a Netty event-loop thread (JDBC, RestTemplate, Thread.sleep, .block()) stalls every connection served by that loop, which can collapse throughput, or deadlock. If unavoidable:
Mono.fromCallable(blockingCall).subscribeOn(Schedulers.boundedElastic()). That's a bounded thread pool (10 × cores by default) designed for blocking work;.block() inside reactive handlers (Reactor throws IllegalStateException on non-blocking threads);If most dependencies are blocking, Spring MVC with virtual threads is the better architecture.
flatMap, concatMap and switchMap?Short answer: All three map each element to an inner publisher, and flatten the results. They differ in concurrency and ordering:
flatMap: subscribes to the inner publishers eagerly and concurrently (bounded by the concurrency parameter, default 256), and interleaves their results. Order isn't preserved. Use it for maximum throughput: parallel API calls per element.concatMap: processes the inner publishers one at a time, in order. It waits for each to complete before starting the next. The order is preserved, and it's slower. Use it for sequential side effects, or ordering-sensitive processing.flatMapSequential: concurrent subscription, but the results are re-ordered to match the source.switchMap: when a new element arrives, it cancels the previous inner publisher, and switches to the new one. Only the latest matters. Use it for search-as-you-type, "latest value" streams, and refresh triggers.Flux<Price> prices = skus.flatMap(sku -> pricing.price(sku), 16); // 16 concurrent calls, unordered
Flux<Result> ordered = commands.concatMap(this::applyInOrder); // strict order
Flux<Suggestions> live = queryChanges.switchMap(q -> search.suggest(q)); // cancel stale searches
Context, and what problems does it solve with ThreadLocal?Short answer: In reactive pipelines, work hops threads (event loops, schedulers). So ThreadLocal-based context (MDC trace IDs, security context, transactions, tenant IDs) gets lost, or leaks between requests that share threads. Reactor Context is an immutable key-value map attached to the subscription:
contextWrite(ctx -> ctx.put("tenant", t)), downstream, and is visible upstream;Mono.deferContextual(ctx -> ...).Bridging to ThreadLocal-based libraries (logging MDC, Micrometer tracing): use context propagation (Hooks.enableAutomaticContextPropagation(), together with Micrometer's context-propagation library). It restores ThreadLocals around operators automatically. Spring Security's WebFlux support and reactive transactions both use the Context.
WebClient and RestTemplate (and RestClient)?Short answer:
RestTemplate: synchronous, blocking, one thread per in-flight call. It's in maintenance mode.RestClient (Spring 6.1): the modern synchronous fluent API, on the same blocking model. It's the recommended choice for MVC, especially with virtual threads, where blocking is cheap.WebClient: non-blocking, reactive, and Netty-based. It handles many concurrent calls with few threads, supports streaming and back-pressure, and composes easily for fan-out (Mono.zip, flatMap). It's the choice in WebFlux, or for massive concurrent fan-out on platform threads.Performance:
Common trap: using WebClient with .block() in an MVC application gains nothing, and adds complexity.
Q: What does "nothing happens until you subscribe" mean in practice?
A: Building a Mono or Flux chain only assembles it. The HTTP call or database query runs when something subscribes (WebFlux does this for handler return values). Forgetting to return or subscribe to a publisher means the work silently never happens.
Q: What are publishOn and subscribeOn?
A: subscribeOn chooses the scheduler for the subscription (it affects the source, wherever it's placed). publishOn switches the thread for downstream operators, from that point on. Use boundedElastic for blocking work, and parallel for CPU work.
Q: Hot vs cold publishers?
A: Cold publishers (most Flux/Mono, like HTTP or database calls) start their work for each subscriber. Hot publishers (Sinks, share(), and live event streams) emit regardless of subscribers, and late subscribers miss the earlier elements.
Q: When is WebFlux clearly the right choice? A: Streaming (SSE, WebSockets), very high concurrency with slow or long-lived connections, API gateways (Spring Cloud Gateway), and end-to-end reactive stacks (reactive database plus reactive messaging).