Future vs CompletableFuture — Blocking Handle vs Composable Async
Fast recall
Quick revision
Core answer
ExecutorService.submit() and call .get() to wait for the result. No callbacks, no composition, no exception handling beyond rethrow on get(), cannot be completed manually. `CompletableFutureFuture but adds a full composable async API: callbacks (thenApply), composition (thenCompose, thenCombine), exception recovery (exceptionally, handle), manual completion (.complete(value)), and executor control (*Async(executor)). Use plain `Future` only when you're legitimately working with an API that returns it and you really do just want to block on the result (batch jobs, simple Callable tasks). Everywhere else use `CompletableFuture` — modern HTTP clients, message handling, parallel fan-out. On Java 21+, for purely I/O-bound code, virtual threads with simple blocking calls often replace CompletableFuture chains entirely.Whether the candidate knows CompletableFuture is a superset (it implements Future) and why that matters; understands the limitations of plain Future that make it unsuitable for non-trivial async code
Thinking Future is asynchronous (it isn't — `.get()` blocks). Using plain Future for complex async flows where composition would help
Note that CompletableFuture implements Future so it's drop-in; mention that virtual threads on Java 21 often replace CompletableFuture chains for I/O-bound code; highlight the commonPool trap with supplyAsync
Interview-ready guide
Interview answer
Future is a blocking result handle; CompletableFuture is a composable async pipeline.
Plain `Future
- You get it from
ExecutorService.submit() - Can call
isDone(),cancel(), or `get()` which blocks - No callbacks, no composition, no exception handling
- Cannot be completed manually
- Fine for: single-task submit-and-block patterns
`CompletableFuture
- Implements
Future— so it's a drop-in replacement - Plus a full composable API:
- Callbacks:
thenApply,thenAccept,thenRun - Composition:
thenCompose(sequential),thenCombine(parallel),allOf/anyOf(fan-in) - Exception handling:
exceptionally,handle,whenComplete - Manual completion:
.complete(value),.completeExceptionally(ex) - Timeouts (Java 9+):
orTimeout,completeOnTimeout - Executor control:
*Async(fn, executor)variants
The `supplyAsync` trap
CompletableFuture.supplyAsync(task)without an executor usesForkJoinPool.commonPool- Sized for CPU work (cores - 1)
- Blocking I/O on commonPool saturates it; all parallel streams stall
- Always pass an executor for blocking work
Java 21 shifts the balance
- Virtual threads make blocking
Future.get()cheap again - Simple blocking code on a virtual thread executor often reads better than CompletableFuture chains
- CompletableFuture remains essential for:
- Wrapping callback-based libraries
- Parallel fan-out with combinators (
allOf(f1, ..., fN)) - Complex pipelines with multi-stage exception recovery
When to use plain Future
- Working with a legacy API that returns Future
- Simple submit-and-block patterns
- Java 21 + virtual threads, where blocking is cheap and composition is overkill
When to use CompletableFuture
- Non-trivial async flows with composition/timeout/recovery
- Wrapping callback-based libraries
- Public API you want callers to chain onto
When neither is the right tool
- Backpressured streaming → Reactor
Flux/RxJavaObservable - Pure CPU work → parallel streams or
ForkJoinPool
Recommendation: On Java 8–17, default to `CompletableFuture` for any async flow with more than one stage; the composition API saves you from callback hell and manual blocking. Always pass a custom executor to *Async methods for blocking work. On Java 21+, re-evaluate: for I/O-bound request handlers, virtual threads with plain blocking `Future.get()` often wins on readability. Reserve CompletableFuture for true parallel fan-out (allOf over many calls) and callback wrapping. Always terminate chains with exceptionally or handle — unterminated chains silently eat exceptions and produce the nastiest kind of production bug.
`Future` vs `CompletableFuture` — Blocking Handle vs Composable Async
Direct Answer
Future<T> (Java 5) is just a blocking result handle — you get it back from ExecutorService.submit() and call .get() to wait for the result. No callbacks, no composition, no exception handling beyond rethrow on get(), cannot be completed manually. CompletableFuture<T> (Java 8) implements Future<T> but adds a full composable async API: callbacks (thenApply), composition (thenCompose, thenCombine), exception recovery (exceptionally, handle), manual completion (.complete(value)), and executor control (*Async(executor)). Use plain Future only when you're legitimately working with an API that returns it and you really do just want to block on the result (batch jobs, simple Callable tasks). Everywhere else use CompletableFuture — modern HTTP clients, message handling, parallel fan-out. On Java 21+, for purely I/O-bound code, virtual threads with simple blocking calls often replace CompletableFuture chains entirely.
Common Mistakes & Pitfalls
Thinking Future is asynchronous (it isn't — .get() blocks). Using plain Future for complex async flows where composition would help
Check your understanding
Answer these without looking back at the article.
- Why is `Future.get()` a blocking call — what was the design intent?
- How does virtual-thread `Future.get()` differ from platform-thread `Future.get()`?
- What happens if you call `.get()` on a CompletableFuture that completed exceptionally?
- Why is `supplyAsync` without an executor dangerous for blocking work?
- How does `CompletionStage` relate to `CompletableFuture`?
Related interview prep categories
2Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.