skip to content

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

level: middleimportance: should knowfreq 48%

answer

  1. Two axes: timing (Both vs Either) x callback (apply/accept/run)
  2. 3x2 grid → 6 methods
  3. apply=Function/value, accept=Consumer/void, run=Runnable/ignore
  4. Both uses BiFunction/BiConsumer (2 results); Either uses one-arg (winner only)
  5. allOf = many Both; anyOf = many Either; each has ...Async

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).

solid answer

~40 s

CompletableFuture's two-input combinators form a tidy 3x2 grid. One axis is *when* the result fires: the 'Both' family (thenCombine, thenAcceptBoth, runAfterBoth) waits for both futures; the 'Either' family (applyToEither, acceptEither, runAfterEither) fires on the first to settle. The other axis is *what the callback returns*: the Combine/ApplyToEither pair takes a function and produces a value; the AcceptBoth/acceptEither pair takes a consumer of the result(s) and returns CompletableFuture<Void>; the runAfterBoth/runAfterEither pair takes a Runnable that ignores results, also Void. Combine uses a BiFunction (two results) while applyToEither uses a Function (only the winner). Each method also has ...Async overloads controlling the executing thread. For more than two inputs, allOf and anyOf are the many-input generalisations of the 'both' and 'either' columns respectively.

go deeper

for a junior

Can name the 'both' and 'either' families and say one waits for both, one for the first.

for a middle

Reproduces the full 3x2 grid, explains the timing and callback axes, and the BiFunction-vs-Function arity difference.

for a senior

Adds the ...Async overloads (which thread runs the callback) and the allOf/anyOf generalisation to many inputs.

for a principal

Reasons about thread-handoff and pool choice across the Async overloads and chooses combinators that minimise context switches and resource waste at scale.

## Why a grid? When you compose **two** `CompletableFuture`s, the API gives you six methods. They look like a confusing list, but they are really **two independent choices**, so they form a clean **3x2 grid**. Knowing the two axes lets you name any method on demand instead of memorising six. ## Axis 1 — When does the continuation fire? - **Both:** wait until **both** input futures complete. Names contain **'Both'** (or 'Combine'). This is a **join**. - **Either:** fire as soon as the **first** input settles; the slower is ignored. Names contain **'Either'**. This is a **race**. ## Axis 2 — What does the callback do with the result(s)? - **Function → value:** you transform the result(s) and the new future holds your returned value. Methods: `thenCombine` / `applyToEither`. - **Consumer → side effect, no value:** you consume the result(s) for an effect; the new future is `CompletableFuture<Void>`. Methods: `thenAcceptBoth` / `acceptEither`. - **Runnable → ignore results entirely:** you just want to run something once the timing condition holds; also `CompletableFuture<Void>`. Methods: `runAfterBoth` / `runAfterEither`. ## The grid | Callback \ Timing | **Both** (wait for both) | **Either** (first to settle) | |---|---|---| | **Function → value** | `thenCombine` (BiFunction) | `applyToEither` (Function) | | **Consumer → side effect** | `thenAcceptBoth` (BiConsumer) | `acceptEither` (Consumer) | | **Runnable → ignore** | `runAfterBoth` (Runnable) | `runAfterEither` (Runnable) | Note the arity difference in the top row: 'Both' sees **two** results, so it uses a **Bi**Function / **Bi**Consumer; 'Either' sees only the **winner**, so a plain Function / Consumer with **one** argument (and both inputs must share a type). ## The naming convention decoded The whole `CompletableFuture` API follows a consistent verb scheme: - `then...` / `...After...` = sequencing. - `apply` = transform with a Function (returns a value). - `accept` = consume with a Consumer (no value). - `run` = a Runnable (ignores inputs). - `Combine` / `Both` / `Either` = the two-input timing word. - `...Async` suffix = run the callback on a supplied executor (or the common pool) instead of the completing thread. So the name *is* the spec: `acceptEither` = 'consume (no value), on whichever finishes first'. ## Async overloads Every one of the six has `...Async` variants (with and without an `Executor`). The non-async form runs the callback on whichever thread completed the triggering input (could be a pool thread or even the caller if already done); the async form hands it to the common ForkJoinPool or your executor — relevant for blocking work or thread-affinity concerns. ## Scaling past two inputs The two columns generalise to **many** inputs: - **Both → many:** `allOf(cfs...)` → `CompletableFuture<Void>` (wait for all; read results from originals). - **Either → many:** `anyOf(cfs...)` → `CompletableFuture<Object>` (first to settle). ## Deriving your answer - Junior: name the two families. - Middle: reproduce the 3x2 grid and explain the two axes plus the BiFunction-vs-Function arity difference. - Senior: add the Async overloads and the allOf/anyOf generalisation.

  • Why does thenCombine take a BiFunction but applyToEither only a Function?
    thenCombine waits for both futures, so its callback receives BOTH results — two arguments → BiFunction. applyToEither fires on the first to settle and only ever sees the winner's single result → a one-argument Function (and both inputs must share that type).
  • What's the difference between the non-async and ...Async versions of these methods?
    The non-async form runs the callback on whatever thread completed the triggering future (or the caller if already complete). The ...Async form submits the callback to the common ForkJoinPool or a supplied Executor, decoupling it from the completing thread — important for blocking work or controlling thread affinity.

saying these in an interview costs you the question

  • Mixing up the arity: using a BiFunction with applyToEither (it takes a one-arg Function)
  • Thinking 'accept' methods return a value — they return CompletableFuture<Void>
  • Forgetting that 'Either' needs both inputs to share a result type
  • Confusing runAfterBoth (timing only) with thenAcceptBoth (consumes the results)

context