TL;DR ⚡
Abstract
Future.get()blocks.CompletableFuturelets you attach callbacks (no thread waits) and compose async steps into a pipeline.- Hook: thenApply = map (value→value) · thenCompose = flatMap (value→future, flattened) · thenCombine = join two futures.
- The work still runs on some pool thread — CF makes your thread non-blocking, not the work itself.
Why it exists (vs plain Future)
Future only lets you submit a task and later block on .get(). You can’t react to completion or chain work without blocking a thread. CompletableFuture fixes two things:
- Callbacks — “when it’s done, run this next” — so no thread sits idle waiting.
- Composition — chain/combine async steps into a pipeline. This is the headline feature.
Careful phrasing: CF doesn’t make work “non-blocking” — the task runs on a pool thread. What changes is you register a continuation instead of parking a thread on
.get().
The key methods
| Method | Function returns | Result | Analogy |
|---|---|---|---|
thenApply(fn) | a value | CF<value> | Stream map |
thenCompose(fn) | a CompletableFuture | CF<value> (flattened) | Stream flatMap |
thenCombine(other, fn) | combines two CFs’ results | CF<combined> | join two |
thenApply vs thenCompose — the trap
If your function itself returns a CompletableFuture and you use thenApply, you get a nested CF<CF<T>> — a future wrapping a future. thenCompose flattens it to a single CF<T>.
- Use thenApply when transforming a value:
CF<String>→ its length. - Use thenCompose when the next step is itself async:
getUserId()(returns CF) thenfetchOrders(id)(also returns CF). - Same trap as
mapvsflatMapgivingStream<Stream<T>>.
thenCombine
For two independent async results you want to merge: fetch price from service A and inventory from service B in parallel, then combine. Different from thenCompose, which is sequential (B depends on A’s result).
Async variants
thenApplyAsync, etc. run the callback on a separate pool thread (default ForkJoinPool.commonPool, or one you pass). Non-Async runs the callback on whatever thread completed the previous stage.
Test Yourself
What does CompletableFuture give you over Future?::Callbacks (no blocking on get) + composition of async steps into a pipeline.
thenApply vs thenCompose?::thenApply: function returns a value → CF
When does thenApply bite you?::When the function you pass itself returns a CompletableFuture — you get a nested CF<CF
thenCombine vs thenCompose?::thenCombine merges two independent futures’ results; thenCompose sequences a future that depends on a previous future’s result.