thenCompose vs thenCombine — Sequential Chain vs Parallel Combine
Fast recall
Quick revision
Core answer
CompletableFuture thenCompose(Function>) . Think of it as async flatMap: it flattens a CompletableFuture> into CompletableFuture. `thenCombine` is for two independent futures running in parallel whose results you combine when both finish. Signature: CompletableFuture thenCombine(CompletionStage, BiFunction) . Rule of thumb: if B needs A's result before starting → thenCompose (sequential). If A and B can run simultaneously and you combine when both done → thenCombine (parallel). Using thenApply where you need thenCompose gives you a nested CompletableFuture> — the classic beginner bug.Whether the candidate understands the flatMap nature of thenCompose vs the parallel combine of thenCombine — a common source of performance bugs (unnecessarily sequential code)
Using thenApply with a function returning CompletableFuture and ending up with a nested CompletableFuture<CompletableFuture<U>>. Also: using thenCompose when the two operations are independent and could run in parallel
Show the wire-diagram intuition: thenCompose = pipe output-to-input (sequential), thenCombine = two parallel wires merging into one. Mention that thenCompose is analogous to monadic bind / flatMap / await-chain
Interview-ready guide
Interview answer
thenCompose and thenCombine handle the two fundamental async topologies.
`thenCompose` — sequential dependent
- Second async op needs first result to start
- Signature:
Function> - Flattens a
CompletableFutureinto> CompletableFuture - Exactly the monadic flatMap / JavaScript await-chain equivalent
- Use when: chained DB → service → cache lookups where each uses the previous
- Latency: sum of all stages (inherent to dependence)
`thenCombine` — parallel independent
- Two futures run simultaneously; combine results when both done
- Signature:
(other, BiFunction) - Use when: fetch orders and preferences for a user in parallel, then build a response
- Latency: max of the two stages (parallel)
The rule of thumb
- Return type of your function tells you which to use
- Function returns
U→thenApply - Function returns
CompletableFuture→thenCompose - You already have two futures →
thenCombine
The two classic bugs
Bug 1 — thenApply instead of thenCompose:
userF.thenApply(user -> findOrdersAsync(user))- Result:
CompletableFuture— double-wrapped>> - Every downstream must
.join().join()or unwrap explicitly - Fix: use
thenCompose
Bug 2 — thenCompose when thenCombine would work:
- Fetching orders and prefs for the same user
- Neither depends on the other, only on user
- Chaining
thenComposeserializes them: time = orders + prefs - Should be
thenCombine: time = max(orders, prefs) - Easy 30–40% latency win for typical dashboards
Mental model
thenCompose≈flatMap/ monadic bind / await-chainthenCombine≈ zip / diamond mergethenApply≈map
Async variants on both
thenComposeAsync(fn, executor)— fn on supplied executorthenCombineAsync(other, fn, executor)— combiner on supplied executor- Use when combiner does non-trivial work; otherwise the non-async form skips a thread hop
For N > 2 parallel operations
- Don't chain
thenCombineN times - Use
CompletableFuture.allOf(f1, f2, f3, ...)then read each result - Cleaner and doesn't build a left-leaning tree
Recommendation: When reviewing async Java, read the function return types — they tell you whether thenApply, thenCompose, or thenCombine is right. Any `thenCompose` chain where the intermediate result isn't used later is a red flag: those stages can probably run in parallel via thenCombine or allOf. Extract long chains into named helper methods; a page-long chain of lambdas is nearly impossible to debug. And on Java 21, ask whether virtual threads with blocking code would be clearer than the CompletableFuture dance — often it will be.
`thenCompose` vs `thenCombine` — Sequential Chain vs Parallel Combine
Direct Answer
thenCompose is for sequential dependent async operations — when the second call needs the first result to start. Signature: CompletableFuture<U> thenCompose(Function<T, CompletableFuture<U>>). Think of it as async flatMap: it flattens a CompletableFuture<CompletableFuture<U>> into CompletableFuture<U>. thenCombine is for two independent futures running in parallel whose results you combine when both finish. Signature: CompletableFuture<V> thenCombine(CompletionStage<U>, BiFunction<T, U, V>). Rule of thumb: if B needs A's result before starting → thenCompose (sequential). If A and B can run simultaneously and you combine when both done → thenCombine (parallel). Using thenApply where you need thenCompose gives you a nested CompletableFuture<CompletableFuture<U>> — the classic beginner bug.
Common Mistakes & Pitfalls
Using thenApply with a function returning CompletableFuture and ending up with a nested CompletableFuture<CompletableFuture<U>>. Also: using thenCompose when the two operations are independent and could run in parallel
Check your understanding
Answer these without looking back at the article.
- What happens if the function passed to `thenCompose` throws synchronously?
- How would you combine 10 parallel CompletableFutures?
- Show me the signature difference between `thenApply` and `thenCompose` — why does it matter?
- When would you use `applyToEither` vs `thenCombine`?
- How do you parallelize a list of async calls and wait for all?
Related interview prep categories
2Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.