Detailed explanation
CompletableFuture, introduced in Java 8, represents a result that may become available later. It implements both Future and CompletionStage, so it supports retrieving a result as well as declaring actions that depend on its completion. Unlike a basic Future, which commonly requires calling get() and waiting, CompletableFuture lets you build a pipeline of dependent operations. This improves asynchronous programming by making sequencing, parallel work, and error recovery explicit without requiring a waiting thread for every dependency.
In the example, supplyAsync(() -> fetchData()) schedules a task and returns a CompletableFuture holding its eventual result. thenApply transforms that result using process(data), while thenAccept consumes the processed value and produces a CompletableFuture<Void>. Each operation returns a new stage that completes after its action finishes. Internally, completion triggers registered dependent actions; a stage can complete normally, exceptionally, or through cancellation. The final exceptionally callback recovers from an earlier exceptional completion by logging the error and returning null, which is appropriate for the Void result here.
Execution context matters. Without an explicit executor, supplyAsync normally uses ForkJoinPool.commonPool(). Non-Async callbacks such as thenApply may execute on the thread completing the preceding stage, or on the registering thread if that stage has already completed. Async variants schedule their actions through an executor. For blocking database or HTTP calls, an appropriately sized dedicated executor helps avoid occupying shared pool workers. CompletableFuture does not automatically make blocking operations non-blocking; get() and join() still wait if the result is unavailable.
For composition, use thenApply when a callback returns a plain value and thenCompose when it returns another CompletionStage, avoiding nested futures. Use thenCombine to combine two independent results, and allOf to create a completion barrier for several futures. Exception handling belongs to the pipeline: exceptionally supplies a fallback, handle transforms either success or failure, and whenComplete observes the outcome. Interviewers judge whether you can explain these distinctions, choose execution contexts deliberately, and avoid immediately blocking after scheduling work, which defeats much of the benefit.
Key points to remember
- CompletableFuture implements Future and CompletionStage, supporting both result retrieval and composable completion actions.
- Use thenApply for value transformations, thenCompose for asynchronous dependencies, and thenAccept for consuming a result.
- Use thenCombine for two independent results; allOf waits for all stages but does not collect their values.
- Non-Async callbacks have no guaranteed dedicated thread; Async variants schedule actions through an executor.
- Handle failures within the pipeline, and reserve blocking get() or join() for boundaries where waiting is necessary.
Common mistakes candidates make
- Using thenApply for a callback returning a future, unintentionally creating a CompletableFuture<CompletableFuture<T>>.
- Assuming every callback runs on a separate thread or that wrapping blocking I/O makes the I/O itself non-blocking.
- Calling join() immediately after each task submission, serialising work that could have overlapped.
Likely follow-up questions
What is the difference between get() and join()?
Both wait for completion if necessary. get() exposes checked InterruptedException and ExecutionException, while join() reports exceptional completion through unchecked CompletionException; both can throw CancellationException.
How do you collect results from multiple CompletableFutures?
Create an allOf barrier and attach a thenApply callback that reads each original future's result. After the barrier completes normally, those futures are complete, so their join() calls do not wait; allOf itself returns CompletableFuture<Void>.
Does cancelling a CompletableFuture interrupt its running task?
No: cancel(true) does not use interruption to stop the underlying computation. Cancellation completes the future exceptionally, so stopping the actual work requires separate task-specific cancellation or cooperative signalling.
Interview tip: Walk through the supplied pipeline, then explain composition, executor selection, and one exception-handling choice to demonstrate practical understanding.
