What is the difference between thenCombine() and thenCompose()
Both methods combine CompletableFutures, but differently:
🟢 Junior Level
Both methods combine CompletableFutures, but differently:
thenCombine()— waits for TWO independent CFs and combines their resultsthenCompose()— waits for ONE CF and launches another CF based on its result
// thenCombine — two CFs, combining results
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> "World");
cf1.thenCombine(cf2, (s1, s2) -> s1 + " " + s2)
.thenAccept(System.out::println); // Hello World
// thenCompose — one CF, then another
CompletableFuture<String> cf = getUserIdAsync();
cf.thenCompose(userId -> getUserAsync(userId))
.thenAccept(user -> System.out.println(user.name()));
🟡 Middle Level
thenCombine — parallel execution
CompletableFuture<User> user = getUserAsync(userId);
CompletableFuture<Order> order = getOrderAsync(userId);
// Both execute in parallel
user.thenCombine(order, (u, o) -> new UserProfile(u, o))
.thenAccept(profile -> System.out.println(profile));
thenCompose — sequential execution
// First getUserId, then getUser
getUserIdAsync()
.thenCompose(userId -> getUserAsync(userId)) // sequential
.thenAccept(user -> System.out.println(user));
Typical mistakes
- Using thenCompose when thenCombine is needed: ```java // ❌ Sequential (slower) getUserAsync(userId) .thenCompose(user -> getOrderAsync(userId));
// ✅ Parallel (faster)
CompletableFuture
---
## 🔴 Senior Level
### Internal Implementation
**thenCombine:**
```java
public <U,V> CompletableFuture<V> thenCombine(
CompletionStage<? extends U> other,
BiFunction<? super T,? super U,? extends V> fn
) {
return biApplyStage(null, other, fn);
}
// Simplified representation of internal JDK methods. Real methods have
// different signatures and additional parameters. Conceptually:
// biApplyStage — creates a CF that waits for BOTH results and applies the function.
thenCompose:
public <U> CompletableFuture<U> thenCompose(
Function<? super T, ? extends CompletionStage<U>> fn
) {
return uniComposeStage(null, fn);
}
// uniComposeStage — creates a CF that waits for the first result and launches the second operation.
Performance
thenCombine:
- Both CFs in parallel
- Latency = max(cf1, cf2)
thenCompose:
- CFs sequential
- Latency = cf1 + cf2
Production Experience
// ✅ thenCombine for parallel calls
CompletableFuture<User> user = getUserAsync(userId);
CompletableFuture<Order> order = getOrderAsync(userId);
user.thenCombine(order, UserProfile::new);
// ✅ thenCompose for dependent calls
getUserIdAsync()
.thenCompose(userId -> getUserAsync(userId));
Best Practices
// ✅ thenCombine for independent CFs
cf1.thenCombine(cf2, combiner);
// ✅ thenCompose for dependent CFs
cf.thenCompose(result -> nextAsync(result));
// ❌ thenCompose for independent calls (slower)
// ❌ thenCompose when CF doesn't depend on the result
🎯 Interview Cheat Sheet
Must know:
- thenCombine — two INDEPENDENT CFs, parallel execution, latency = max(cf1, cf2)
- thenCompose — DEPENDENT CFs, sequential execution, latency = cf1 + cf2
- thenCombine takes BiFunction<T, U, V>, thenCompose takes Function<T, CompletionStage>
- thenCompose is analogous to flatMap, thenCombine is analogous to zip
- Common mistake: using thenCompose for independent CFs — unnecessarily slow
Common follow-up questions:
- thenCombine for dependent CFs? — No, thenCombine cannot use the result of the first in the second
- thenCompose for independent CFs? — Works, but sequentially (slower). Better use thenCombine
- thenCombine vs allOf? — thenCombine for two CFs with a combiner, allOf for N CFs (returns Void)
- Internal implementation? — thenCombine: BiRelay waits for both CFs. thenCompose: uniComposeStage waits for the first, launches the second
Red flags (DO NOT say):
- “thenCombine and thenCompose are interchangeable” — different semantics: parallel vs sequential
- “thenCompose is faster” — thenCompose is sequential (sum latency), thenCombine is parallel (max latency)
- “thenCombine takes a Function” — it takes a BiFunction from both CF results
Related topics:
- [[4. What is the difference between thenApply() and thenCompose()]]
- [[8. How to combine results of multiple CompletableFuture]]
- [[9. What does allOf() method do and when to use it]]
- [[16. How to properly execute multiple parallel requests to microservices]]