⏱️ Section 19 · Question #9

What Does allOf() Method Do and When to Use It

The static utility method CompletableFuture.allOf(CompletableFuture... cfs) is used to run an arbitrary number ($N$) of independent asynchronous operations in parallel and aw...


🟢 Junior Level

The static utility method CompletableFuture.allOf(CompletableFuture<?>... cfs) is used to run an arbitrary number ($N$) of independent asynchronous operations in parallel and await the completion of all of them before taking action.

The method returns a new CompletableFuture<Void> that transitions to the completed state only after every passed future has finished execution.

Basic Code Example

CompletableFuture<String> priceFuture = CompletableFuture.supplyAsync(() -> fetchPrice());
CompletableFuture<String> stockFuture = CompletableFuture.supplyAsync(() -> fetchStock());
CompletableFuture<String> reviewsFuture = CompletableFuture.supplyAsync(() -> fetchReviews());

// Combine all 3 parallel tasks into a single completion barrier:
CompletableFuture<Void> allTasks = CompletableFuture.allOf(priceFuture, stockFuture, reviewsFuture);

// Triggers ONLY after ALL 3 tasks have finished:
allTasks.thenRun(() -> {
    System.out.println("All product details loaded successfully!");
    // Calling .join() here is non-blocking and safe because all tasks are guaranteed complete:
    String price = priceFuture.join();
    String stock = stockFuture.join();
    String reviews = reviewsFuture.join();
    renderProductPage(price, stock, reviews);
});

[!IMPORTANT] allOf() returns CompletableFuture<Void>, not a collection of results. The results of individual futures must be extracted via .join() on the original future instances.


🟡 Middle Level

Idiomatic Pattern: Asynchronously Collecting Results into List<T>

In production applications, task counts are typically dynamic (such as batch processing a list of entity identifiers). The standard pattern converts List<CompletableFuture<T>> into CompletableFuture<List<T>>:

public <T> CompletableFuture<List<T>> fetchAll(List<Long> ids, Function<Long, CompletableFuture<T>> taskFactory) {
    // 1. Instantiate the list of asynchronous futures
    List<CompletableFuture<T>> futures = ids.stream()
        .map(taskFactory)
        .toList();

    // 2. Construct the allOf synchronization barrier
    return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
        .thenApply(ignoredVoid -> 
            futures.stream()
                // join() is safe and non-blocking: allOf guarantees each future isDone()
                .map(CompletableFuture::join)
                .toList()
        );
}

Failure Semantics and Error Isolation

By default, allOf exhibits critical error behavior:

  • If any single future completes exceptionally, the aggregated allOf future also completes exceptionally.
  • Calling allOf.join() throws a CompletionException wrapping the exception of whichever future failed first chronologically.
  • Results from all other successfully completed futures are discarded unless handled locally.
// Solution: Isolate errors at the individual task level via fallbacks
CompletableFuture<User> safeUserFuture = userService.getUserAsync(id)
    .exceptionally(ex -> User.ANONYMOUS); // Local fallback

CompletableFuture<Account> safeAccountFuture = accountService.getAccountAsync(id)
    .exceptionally(ex -> Account.DEFAULT); // Local fallback

// allOf is now guaranteed to complete successfully without failing the batch:
CompletableFuture.allOf(safeUserFuture, safeAccountFuture)
    .thenAccept(v -> showProfile(safeUserFuture.join(), safeAccountFuture.join()));

🔴 Senior Level

Internal Architecture: The BiRelay Binary Tree

Unlike legacy thread-blocking concurrency constructs like CountDownLatch, CompletableFuture.allOf uses a lock-free, zero-thread-pinning algorithm.

When passed an array of $N$ futures, allOf recursively constructs a balanced binary tree of BiRelay completion nodes:

// Conceptual implementation from java.util.concurrent.CompletableFuture:
public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs) {
    return andTree(cfs, 0, cfs.length - 1);
}

static CompletableFuture<Void> andTree(CompletableFuture<?>[] cfs, int lo, int hi) {
    CompletableFuture<Void> d = new CompletableFuture<>();
    if (lo > hi) {
        d.result = NIL;
    } else {
        int mid = (lo + hi) >>> 1;
        CompletableFuture<?> a = (lo == mid ? cfs[lo] : andTree(cfs, lo, mid));
        CompletableFuture<?> b = (lo == hi ? a : (hi == mid + 1 ? cfs[hi] : andTree(cfs, mid + 1, hi)));
        // Links children to parent via internal BiRelay nodes...
    }
    return d;
}
  1. The dependency tree has a maximum depth of $O(\log N)$.
  2. As each leaf future finishes, completion signals propagate up the tree.
  3. Whichever worker thread completes the final leaf node in the tree completes the root future.

The Fail-Fast Myth: Does allOf Complete Immediately on Error?

A widespread misconception is that allOf is fail-fast: “as soon as one task fails, allOf immediately completes with an exception”. In standard OpenJDK, this is FALSE.

The andTree structure awaits the physical completion (successful or exceptional) of every single sub-task. The root node records the exception, but it will not complete until all remaining $N - 1$ tasks have finished execution in their respective thread pools. If one sub-task hangs indefinitely (e.g., an un-timeouted socket read), the entire allOf future hangs indefinitely, even if another task failed seconds earlier. Always attach timeouts (orTimeout) to individual sub-tasks!


4 Tricky Questions

1. What happens if you pass an empty array or zero arguments to CompletableFuture.allOf()?

Answer: The method completes immediately and successfully, returning a CompletableFuture<Void> that is already completed with null (completedFuture(null)). No exceptions (IllegalArgumentException or ArrayIndexOutOfBoundsException) are thrown. This guarantees safety when passing dynamic collections of zero size: CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) seamlessly completes without special guard clauses.

2. If 3 out of 10 futures passed to allOf fail with different exceptions, how can you collect all 3 errors rather than just the first?

Answer: allOf.join() or allOf.exceptionally() only exposes the chronologically first exception. The other two exceptions are inaccessible from the root allOf instance. To capture all failures, attach a .whenComplete handler to allOf and iterate over the original collection of futures:

CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
    .whenComplete((v, overallEx) -> {
        List<Throwable> allErrors = futures.stream()
            .filter(CompletableFuture::isCompletedExceptionally)
            .map(f -> {
                try { f.join(); return null; }
                catch (CompletionException ce) { return ce.getCause(); }
            })
            .filter(Objects::nonNull)
            .toList();
        log.error("Total errors in batch: {}", allErrors.size());
    });

3. Why is calling .join() inside a .thenApply(...) chained to allOf not considered a blocking code smell?

Answer: Calling .join() is an anti-pattern when invoked on an in-flight future because it blocks the calling thread. However, inside a callback chained to allOf, the callback only triggers after allOf has completed. Since allOf only completes when all children are finished, each child future is guaranteed to have isDone() == true. Under these conditions, .join() never parks the thread; it simply reads the volatile result field in 1–2 nanoseconds.

4. If you invoke allDone = CompletableFuture.allOf(f1, f2) and subsequently call allDone.cancel(true), are sub-tasks f1 and f2 cancelled?

Answer: No, neither f1 nor f2 will be cancelled. In CompletableFuture, cancellation propagates strictly downstream, never upstream. Calling cancel(true) on allDone only marks allDone itself as exceptionally completed with a CancellationException. The underlying worker tasks executing f1 and f2 continue running in their thread pools to completion. To cancel sub-tasks, cancel() must be called explicitly on each child future.


🎯 Interview Cheat Sheet

Summary in 60 Seconds

  • CompletableFuture.allOf() awaits completion of all passed futures ($N$ tasks).
  • Returns CompletableFuture<Void>. Extract individual values using futures.stream().map(CompletableFuture::join).toList().
  • Overall latency equals the duration of the slowest task: $\max(t_1, t_2, \dots, t_N)$.
  • Built internally as a balanced binary tree of BiRelay nodes with $O(\log N)$ depth.
  • allOf is not fail-fast: it waits for all tasks to finish even if one fails. Always apply individual timeouts (orTimeout) to avoid hanging.

Common Interview Mistakes

  • Expecting allOf to return a CompletableFuture<List<T>> automatically.
  • Believing allOf cancels remaining tasks when one task fails.
  • Failing to protect individual tasks with timeouts, causing one stuck request to block the entire batch forever.