CompletableFuture: Async Composition and Failure Handling

Java's CompletableFuture represents a result that may become available later and provides operations for composing asynchronous work. In backend interviews, the key skills are choosing the right composition method, controlling execution, and handling failures without accidentally blocking.

A `CompletableFuture<T>` is both a holder for a future result and a stage in an asynchronous computation. Unlike a plain `Future`, it supports registering dependent actions that run when the computation completes, allowing workflows to be expressed as a pipeline rather than repeatedly calling `get()`.

Use `thenApply` when a completed value should be transformed into another value. Use `thenAccept` when the next action consumes the value without producing a result, and `thenRun` when the next action needs neither the value nor a returned result. These methods are useful for expressing sequential dependencies between stages.

For independent asynchronous operations, `thenCombine` combines the results of two stages, while `allOf` waits for multiple futures to complete but returns a `CompletableFuture<Void>`. For branching based on either of two completed stages, `applyToEither` or related `*Either` methods can be appropriate.

Failure handling is part of the pipeline. `exceptionally` can recover from an exception by producing a replacement value, `handle` receives either the successful result or the failure and can map both cases to a new result, while `whenComplete` observes completion without normally changing the result. A recovery stage only handles failures that reach that stage in the chain.

Execution semantics matter in interviews. Non-async continuation methods may execute using the thread that completes the preceding stage, whereas methods such as `thenApplyAsync` schedule the continuation for asynchronous execution, using the common pool by default unless an explicit `Executor` is supplied. Blocking calls such as `get()` and `join()` can undermine an otherwise asynchronous design and should be used deliberately.

Key points

Interview questions

What is the difference between thenApply and thenApplyAsync?

Both transform the result of a completed stage, but their execution behavior differs. A non-async continuation may run on the thread that completes the preceding stage. `thenApplyAsync` schedules the continuation asynchronously, using the default asynchronous execution facility unless an executor is provided. In production systems, an explicit executor can make thread-pool ownership and resource limits clearer.

What problem does CompletableFuture solve compared with Future?

A `Future` lets a caller represent an asynchronous result, but retrieving that result commonly involves blocking with `get()`. `CompletableFuture` adds dependent actions, so later work can be registered to run when an earlier stage completes. It also supports composition, combination, and failure handling. This makes multi-step asynchronous workflows easier to express without blocking at every step.

What is the difference between thenApply, thenCompose, and thenCombine?

`thenApply` transforms the value from one completed stage into another value. `thenCompose` is for a transformation that itself returns a `CompletableFuture`, and it flattens the nested asynchronous result into one future. `thenCombine` is used when two independent futures should both complete before their results are combined. The choice depends on whether the second operation is value transformation, dependent asynchronous work, or independent asynchronous work.

How do you handle exceptions in a CompletableFuture pipeline?

The appropriate operator depends on whether the code should recover, transform, or merely observe failure. `exceptionally` can turn an exception into a fallback value, while `handle` receives both the successful result and exception and can produce a new result from either. `whenComplete` is useful for side effects such as logging or metrics while preserving the existing outcome unless the callback itself introduces an exceptional completion. Failure handling should be placed at the stage where the required recovery or observation belongs.

How would you run several independent backend calls concurrently and wait for all of them?

I would start the independent operations before combining them so their work can overlap. `CompletableFuture.allOf` can represent completion of the group, although its result is `Void`, so the individual futures must still be consulted when their values are needed. I would also consider timeout, cancellation, exception policy, and executor sizing rather than treating concurrency as free. If partial results are acceptable, I would design explicit per-call fallback behavior instead of making the whole operation fail unnecessarily.