skip to content

Combining Multiple Futures

Joining independent stages with thenCombine (both) or acceptEither (first to finish), and aggregating many with allOf and anyOf. Interviewers like allOf because it returns Void, so you must know how to get the results back out.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

5

What does thenCombine do on a CompletableFuture, and when would you use it?

level: juniorimportance: must knowfreq 70%

answer

  1. Two independent futures → one combined value
  2. BiFunction(a, b) -> result
  3. Both must be started first → runs in parallel
  4. Either fails → combined fails, fn skipped
  5. Combine=value, AcceptBoth=side effect, runAfterBoth=ignore results

basics

~10 s

thenCombine waits for two independent CompletableFutures to both finish, then merges their two results into one new value using a function you supply.

solid answer

~40 s

thenCombine joins two independent CompletableFutures. You call it on one future, pass the other future plus a BiFunction; when both complete, the BiFunction receives both results and you return a combined value, which becomes the result of the new future. The two stages run concurrently (you start them before combining), so the total time is roughly the slower of the two rather than their sum. Use it when you need data from two unrelated async calls together, e.g. fetch a user and their account balance in parallel, then build a profile object. If either source future fails, the combined future completes exceptionally and the BiFunction is skipped. Variants: thenAcceptBoth takes a BiConsumer (returns CompletableFuture<Void>, no result) and runAfterBoth takes a Runnable (ignores both results) when you only care that both finished.

go deeper

for a junior

Knows thenCombine waits for two futures and merges their results with a function, and can write a basic supplyAsync + thenCombine example.

for a middle

Explains the parallelism (both must be started first → latency is the max, not the sum), failure short-circuiting, and the thenAcceptBoth/runAfterBoth distinction.

for a senior

Discusses async vs non-async overloads and which thread the callback runs on, contrasts thenCombine with thenCompose, and handles exceptions via handle/exceptionally downstream.

for a principal

Reasons about pool sizing and thread-handoff costs of the Async variants at scale, and when thenCombine's pairwise composition should give way to allOf-based aggregation for many stages.

## Background: what a CompletableFuture is A `CompletableFuture<T>` is a Java object representing a value of type `T` that may not exist yet because it is being computed asynchronously (often on another thread). It is a 'promise' of a future result. You attach callbacks that run automatically when the value becomes available, instead of blocking and waiting. ## The problem thenCombine solves Suppose you have **two independent** asynchronous computations — neither depends on the other's result. For example, calling two different web services. You want to start both, let them run **at the same time**, and then do something with **both** results once they are both ready. `thenCombine` is the tool for exactly this. ## Signature and meaning ``` <U,V> CompletableFuture<V> thenCombine( CompletionStage<? extends U> other, BiFunction<? super T, ? super U, ? extends V> fn) ``` Read it as: 'I am a future producing a `T`. Here is `other`, another future producing a `U`. When **both** of us have completed, call `fn(myResult, otherResult)` and the value it returns (a `V`) becomes the result of the **new** future I hand back.' - `other` is the second future. A `CompletionStage` is the interface `CompletableFuture` implements; just think 'another future'. - `fn` is a `BiFunction` — a function taking **two** arguments and returning one. (A `Function` takes one argument; a `BiFunction` takes two.) ## Why it runs in parallel The two futures must already be **started** before you combine them. Typically: ```java CompletableFuture<Integer> a = CompletableFuture.supplyAsync(() -> slowA()); CompletableFuture<Integer> b = CompletableFuture.supplyAsync(() -> slowB()); CompletableFuture<Integer> sum = a.thenCombine(b, (x, y) -> x + y); ``` Because `a` and `b` were both submitted to a thread pool (via `supplyAsync`) before the `thenCombine` line, they execute **concurrently**. The combining function only fires after the later of the two finishes, so total latency ≈ max(timeA, timeB), not timeA + timeB. (Pitfall: if you accidentally chain `b` off `a`, they become sequential and you lose the parallelism.) ## What happens on failure If **either** `a` or `b` completes exceptionally (throws), the returned future also completes exceptionally with that exception, and `fn` is **never** called. You handle that downstream with `exceptionally`, `handle`, or `whenComplete`. ## The sibling 'both' methods The same 'wait for both' idea comes in three flavours that differ only in what they do with the two results: | Method | Takes | Produces | Use when | |---|---|---|---| | `thenCombine` | `BiFunction<T,U,V>` | `CompletableFuture<V>` | you need a **combined value** | | `thenAcceptBoth` | `BiConsumer<T,U>` | `CompletableFuture<Void>` | you consume both for a **side effect**, no value | | `runAfterBoth` | `Runnable` | `CompletableFuture<Void>` | you only care that **both finished**, ignore results | (Each also has `...Async` overloads that run the callback on a supplied or common pool thread rather than on whichever thread completed the last input.) ## Deriving your answer - Junior: 'it waits for two futures and merges their results with a function.' - Senior: add the parallelism guarantee, the failure semantics, and the AcceptBoth/runAfterBoth distinction.

  • How is thenCombine different from thenCompose?
    thenCombine joins TWO already-running futures with a BiFunction and merges their results. thenCompose takes ONE future and a function that itself returns a future, flattening the nesting (a flatMap) for sequential dependent steps — the second call depends on the first's result.
  • If you only need to know that both futures finished but don't care about their values, what do you use?
    runAfterBoth, which takes a Runnable and returns CompletableFuture<Void>.

saying these in an interview costs you the question

  • Thinking thenCombine starts the second future — both must already be running for parallelism
  • Confusing thenCombine (two futures, BiFunction) with thenCompose (one future, flatMap of a function returning a future)
  • Claiming the combining function runs even if one input fails — it does not
  • Chaining the second future off the first, which makes them sequential not parallel

context

open as a page

Why does CompletableFuture.allOf return CompletableFuture<Void>, and how do you get the actual results out?

level: seniorimportance: must knowfreq 66%

basics

~10 s

allOf can take futures of different types, so it can't return one combined value — it returns Void, signalling only that all finished. You read each original future's result yourself afterward (e.g. with join).

open as a page

Map the full grid of CompletableFuture two-input combinators by what they wait for and what callback they take.

level: middleimportance: should knowfreq 48%

basics

~20 s

There are six two-input methods in a 3x2 grid: wait for BOTH (thenCombine, thenAcceptBoth, runAfterBoth) or EITHER first (applyToEither, acceptEither, runAfterEither). Within each row the callback is a function (returns a value), a consumer (side effect), or a runnable (ignores results).

open as a page

What do applyToEither and acceptEither do, and how do they differ from the 'both' family?

level: middleimportance: should knowfreq 52%

basics

~10 s

The 'either' methods react to whichever of two futures finishes first. applyToEither passes that first result to a function; the 'both' methods instead wait for both futures to complete.

open as a page

What are the typing and design trade-offs of CompletableFuture.anyOf, and when would you reach for it?

level: principalimportance: should knowfreq 40%

basics

~20 s

anyOf completes as soon as the first of many futures finishes, returning that result. Because the inputs may differ in type, its result type is Object, so you usually cast or restrict inputs to one type.

open as a page