When is it better to use CompletableFuture vs reactive programming
CompletableFuture covers 90% of microservice tasks:
🟢 Junior Level
Short answer
- CompletableFuture — when you need to make one async request and get one result.
- Reactive programming (Reactor, RxJava) — when you need to process a stream of data (0..N elements).
// CompletableFuture — one request, one response
CompletableFuture<User> cf = userService.findByIdAsync(1L);
User user = cf.join();
// Reactor — stream of elements
Flux<Event> events = eventService.streamAll();
events.subscribe(System.out::println);
Rule of thumb: If you need 2-3 parallel API calls — go with CompletableFuture. If you have data streams — go with Reactor.
🟡 Middle Level
When CompletableFuture is enough
// Typical use case: parallel calls
CompletableFuture<User> userFuture = userClient.getByIdAsync(userId);
CompletableFuture<Order> orderFuture = orderClient.getLatestAsync(userId);
CompletableFuture<UserWithOrder> combined = userFuture.thenCombine(
orderFuture, UserWithOrder::new
);
CompletableFuture covers 90% of microservice tasks:
- Hit the DB
- Call an external API
- Combine multiple results
When you need reactivity
1. Backpressure
CompletableFuture has no backpressure mechanism. If the producer generates data faster than the consumer can process, the queue in the Executor grows until OutOfMemoryError.
Reactive Flux allows the consumer to request as many elements as it can handle:
// Consumer controls the rate
flux.subscribe(element -> process(element), error -> {}, () -> {},
subscription -> subscription.request(10)); // only 10 elements
2. Stream processing
If you need to process data as it arrives (chunk by chunk from S3, Kafka topics, WebSockets), CompletableFuture forces you to either collect everything in memory or write complex recursive chains. Flux does this out of the box.
Comparison
| Characteristic | CompletableFuture | Reactive (Reactor) |
|---|---|---|
| Results | Exactly 1 | 0 … ∞ |
| Backpressure | No | Yes |
| Operators | ~20 | ~200 |
| Learning curve | Low | High |
| Debugging | Medium | Difficult |
🔴 Senior Level
Impact of Virtual Threads (Java 21+)
With Virtual Threads, the need for reactive programming in typical I/O tasks is reduced:
// Instead of reactive code — simple synchronous code
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
User user = userClient.getById(userId); // blocking, but cheap
Order order = orderClient.getLatest(userId); // scales
return new UserWithOrder(user, order);
}
Virtual Threads make synchronous blocking code scalable. Reactivity remains relevant only where these are important:
- Event-driven nature (streaming, WebSockets)
- Backpressure (load control)
Project Loom and Project Valhalla
Project Loom (Virtual Threads) is available in Java 21 and simplifies writing scalable blocking code.
Project Valhalla (value types) is in development — potentially reduces CompletableFuture overhead through primitive specialization, but this is not confirmed.
Final Decision Matrix
- CompletableFuture — 2-3 parallel API calls, combine results.
- CompletableFuture + Virtual Threads — scalable blocking operations.
- Reactive — gateway for millions of events, Kafka Streams, backpressure needed.
Summary
- CF — for async requests.
- Reactive — for async streams.
- Don’t use reactivity where CF is sufficient; the maintenance complexity of reactive code is enormous.
🎯 Interview Cheat Sheet
Must know:
- CompletableFuture — 1 request, 1 result. ~20 operators, low learning curve
- Reactive (Reactor) — 0..N elements, backpressure, ~200 operators, high learning curve
- CF for 2-3 parallel API calls. Reactor for streaming, Kafka, WebSocket
- Virtual Threads (Java 21+) reduce the need for Reactor in typical I/O tasks
- Decision Matrix: CF → CF + Virtual Threads → Reactive (by complexity)
Common follow-up questions:
- When to choose CompletableFuture? — 2-3 parallel API calls, combine results
- When Reactive? — Backpressure needed, event-driven nature (Kafka, WebSocket), millions of events
- Do Virtual Threads kill Reactor? — No. VT for I/O blocking, Reactor for streaming + backpressure
- What is backpressure? — Consumer controls the rate: request(N) elements, prevents OOM
Red flags (DO NOT say):
- “Reactive is always better than CompletableFuture” — maintenance complexity is huge, CF is enough for 90% of tasks
- “Virtual Threads replace Reactive” — no, VT don’t provide backpressure or streaming
- “CompletableFuture supports backpressure” — no, under overload the queue grows to OOM
Related topics:
- [[2. What are the main advantages of CompletableFuture over Future]]
- [[8. How to combine results of multiple CompletableFuture]]
- [[12. What thread pool is used by default for async methods]]
- [[24. How to test code with CompletableFuture]]