Back to Reactive & Flow API
Q2Question 2 of 2

Flow API vs Reactor vs RxJava?

Key Concepts
  • Flow API, Reactor, and RxJava all implement the same Reactive Streams specification and interoperate.
  • Flow has no operators; Reactor (Flux/Mono) and RxJava (Flowable/Observable) add map, filter, flatMap.
  • CompletableFuture models a single async value, not a backpressured stream of many items.
  • Virtual threads (Java 21) let blocking code scale cheaply, removing much of the reason for reactive.
  • Choose reactive for continuous streams with backpressure: SSE, Kafka pipelines, WebSocket fan-out.
  • Choose CompletableFuture for a single async result or a handful of parallel calls to fan out and join.
  • Reactive and virtual threads are not mutually exclusive: reactive wins for true streaming, virtual threads win for request-per-task.

Definition

Flow, Reactor and RxJava speak the same Reactive Streams contract; reactive is for backpressured streams, CompletableFuture is for a single async result, and virtual threads let simple blocking code scale so you only go reactive when you truly need streaming backpressure.
  • Flow API – the lowest common denominator: four interfaces, no operators, no runtime.
  • Reactor – Flux (0..N items) and Mono (0..1 item) with a rich operator set, used by Spring WebFlux.
  • RxJava – Flowable (backpressured) and Observable (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.

JAVA
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.

JAVA
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.

JAVA
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.

JAVA
// 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.

JAVA
// 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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate