Combining futures

Read the explanation
What if

Dependent calls run one after another. Independent calls run side by side, then merge.

0123456789101112main: start chainjoin()start bothjoin()fetchUserfetchLastOrderfetchUserfetchWeatherrender

Tap any block to see what that thread is doing.

  • Running
  • Waiting / parked
  • Waiting on I/O
Step 1/5

thenCompose: fetchLastOrder needs the user, so it can't start until fetchUser is done.

1// thenCompose: step 2 needs step 1's result, so they run one after another2Order last = fetchUser(42)3    .thenCompose(user -> fetchLastOrder(user))4    .join();5 6// thenCombine: two independent calls run at the same time, then merge7String page = fetchUser(42)8    .thenCombine(fetchWeather(), (user, weather) -> render(user, weather))9    .join();

Explanation

What just happened

thenCompose ran two calls one after another, because the second call needed the first one’s result (the user). Total wait: both calls added together.

thenCombine started two calls at once, because the weather doesn’t depend on the user. When both were done, render got both results. Total wait: the slower of the two.

Try the what-if switch

allOf fan-out: three independent price calls start together, and allOf(a, b, c).join() waits for the last one. Total time is the slowest call, not the sum. After that, each join() returns at once.

How to choose

Ask: does call B need A’s result?

Why not thenApply?

fetchUser(42).thenApply(user -> fetchLastOrder(user))
// CompletableFuture<CompletableFuture<Order>>: a future inside a future

When your function already returns a future, thenCompose flattens it, like flatMap on streams.

Watch out

Common questions

What is the difference between thenCompose and thenCombine?

thenCompose chains a call that needs the previous result, so the calls run one after another. thenCombine merges two independent futures that run side by side, so you wait only for the slower one.

What is the difference between thenApply and thenCompose?

Use thenCompose when your function returns a CompletableFuture. With thenApply you'd get a future inside a future, CompletableFuture<CompletableFuture<T>>. It's like map vs flatMap.

How do I wait for all CompletableFutures to finish?

Start them all, then call CompletableFuture.allOf(a, b, c).join(). It takes as long as the slowest call. allOf returns CompletableFuture<Void>, so read each result with join() on the original futures.

What happens to allOf when one future fails?

The combined future fails too, but only after all the futures have finished.

Verified: this timeline matches a real program, checked in CI on JDK 21 and 25. How we verify. Unclear or wrong? Tell us