Question 23 · Section 19

What is the difference between thenCombine() and thenCompose()

Both methods combine CompletableFutures, but differently:

Language versions: English Russian Ukrainian

🟢 Junior Level

Both methods combine CompletableFutures, but differently:

  • thenCombine() — waits for TWO independent CFs and combines their results
  • thenCompose() — 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

  1. Using thenCompose when thenCombine is needed: ```java // ❌ Sequential (slower) getUserAsync(userId) .thenCompose(user -> getOrderAsync(userId));

// ✅ Parallel (faster) CompletableFuture user = getUserAsync(userId); CompletableFuture order = getOrderAsync(userId); user.thenCombine(order, (u, o) -> new Summary(u, o));


---

## 🔴 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]]