Definition
- Flow API – the lowest common denominator: four interfaces, no operators, no runtime.
- Reactor –
Flux(0..N items) andMono(0..1 item) with a rich operator set, used by Spring WebFlux. - RxJava –
Flowable(backpressured) andObservable(no backpressure) with operators and schedulers. - CompletableFuture – models a single asynchronous value with composition (
thenApply,thenCompose,thenCombine). - Virtual threads (Java 21) – lightweight threads that make blocking code scale cheaply, removing much of the concurrency motivation for reactive.
Think of the Reactive Streams contract as the standard shape of a cheque that every bank in India agrees on. The Flow API is the bare blank cheque: valid, but you fill everything by hand. Reactor and RxJava are pre-printed cheque books with helpful boxes (operators). CompletableFuture is a single demand draft: one amount, one payee. Virtual threads are like hiring ten thousand cheap tellers so you can just let each customer wait at a counter, without the reactive machinery at all.
Understanding the Concepts
How do Flow API, Reactor, and RxJava relate?
All three implement the same Reactive Streams specification, so they interoperate. Reactor's JdkFlowAdapter.flowPublisherToFlux(jdkPublisher) turns a JDK Flow.Publisher into a Reactor Flux. RxJava's Flowable implements the Publisher interface directly.
Why use Reactor or RxJava instead of raw Flow?
Raw Flow has no operators. Reactor gives declarative operators that preserve backpressure automatically.
Flux.from(jdkPublisher)
.map(txn -> enrich(txn))
.filter(txn -> txn.getAmount().compareTo(LIMIT) > 0)
.flatMap(txn -> scoreAsync(txn), 4)
.retryWhen(Retry.backoff(3, Duration.ofMillis(100)))
.subscribe(txn -> log.info("Scored: {}", txn));
When do you choose CompletableFuture?
CompletableFuture represents one future value. Use it for a single result or a fixed set of parallel calls.
package com.bank.service;
import java.math.BigDecimal;
import java.util.concurrent.CompletableFuture;
public class DashboardService {
public CompletableFuture<Dashboard> loadDashboard(String accountId) {
var balanceFuture = CompletableFuture.supplyAsync(
() -> accountRepo.getBalance(accountId));
var statementFuture = CompletableFuture.supplyAsync(
() -> statementRepo.getLatest(accountId));
return balanceFuture.thenCombine(statementFuture,
(balance, statement) -> new Dashboard(balance, statement));
}
}
When do virtual threads make reactive unnecessary?
Virtual threads let you write ordinary blocking code that scales. Blocking a virtual thread parks it cheaply instead of pinning an OS thread.
try (var executor = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor()) {
for (String accountId : allAccounts) {
executor.submit(() -> {
var account = accountRepo.findById(accountId); // blocking, but cheap
var score = fraudClient.score(account); // blocking, but cheap
return process(account, score);
});
}
}
How do you decide between reactive, CompletableFuture, and virtual threads?
| Scenario | Tool | Reason |
|----------|------|--------|
| Continuous stream with backpressure | Reactor / RxJava | Need operators and demand control |
| Single async result | CompletableFuture | One value, simple composition |
| Fan-out a few parallel calls | CompletableFuture | thenCombine joins results |
| High-concurrency blocking I/O | Virtual threads | Blocking code that scales |
| SSE, WebSocket, Kafka pipeline | Reactor | True streaming semantics |
What are the trade-offs of reactive code?
Reactive code is harder to debug because stack traces are fragmented, and you must avoid blocking calls inside the pipeline. Virtual threads remove the debugging pain by keeping simple, stack-trace-friendly blocking code.
What are common mistakes?
The wrong approach is using reactive just for thread efficiency when virtual threads are available.
// WRONG: reactive only for thread savings, not for streaming
Mono.fromCallable(() -> accountRepo.findById("SAV2001"))
.subscribeOn(Schedulers.boundedElastic())
.block(); // defeats the purpose
The correct approach uses virtual threads for simple request-per-task.
// CORRECT: blocking code on virtual thread
var account = accountRepo.findById("SAV2001"); // simple, debuggable
What are common interview follow-up questions?
- Can you mix reactive and virtual threads in the same application?
- How does Spring WebFlux relate to Reactor?
- When would virtual threads NOT help?
Visual Overview
Reactive Streams Specification (4 interfaces)
│
┌────┴────────────────────────────────┐
│ │
▼ ▼
Flow API Reactor / RxJava
(bare interfaces) (interfaces + operators)
no map, filter map, filter, flatMap,
no runtime retry, window, buffer
CompletableFuture Virtual Threads
(single async value) (blocking code that scales)
thenApply, thenCombine Executors.newVirtualThreadPerTaskExecutor()
no backpressure no reactive overhead
Decision:
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Stream + back- │ │ Single value │ │ Blocking I/O │
│ pressure needed? │ │ or fan-out? │ │ at scale? │
│ → Reactor/RxJava │ │ → CompletableFut │ │ → Virtual threads│
└──────────────────┘ └──────────────────┘ └──────────────────┘
Key Takeaway
The JDK Flow API, Project Reactor, and RxJava all implement the identical Reactive Streams specification. The Flow interfaces are the lowest common denominator with no operators at all. Reactor with Flux and Mono, and RxJava with Flowable, add the rich operator libraries.
I choose a reactive library when the problem is genuinely a continuous stream that needs backpressure and rich operators. I choose CompletableFuture when I have a single asynchronous result or a handful of independent calls to fan out and join. The biggest shift is virtual threads in Java 21, which let me write blocking code that scales, removing much of the reason to go reactive for thread efficiency.
At National Bank of India I now pick by the shape of the problem: reactive for the settlement feed that needs backpressure, CompletableFuture for the dashboard that fans out two calls, and virtual threads for the batch processor that runs blocking I/O at scale.
Remember one sentence: Flow, Reactor, and RxJava share the Reactive Streams contract; use reactive for backpressured streams, CompletableFuture for a single value, and virtual threads when blocking code needs to scale.
---