CompletableFuture represents an eventually completed result and allows dependent computations to be composed without manually nesting every callback.
Java CompletableFuture: composition, failures and executor ownership
Java 8+. The program uses only JDK classes and runs without a framework.
Compose dependencies, not waiting threads
A shipment quote depends on a base rate, while the declared-insurance calculation can be obtained independently. thenCombine creates a stage that waits for both results before computing the total. It describes a dependency without making the combining expression call get on the other future.
thenApply maps a result into another value. thenCompose is needed when that mapping itself returns a future and the caller wants one flattened dependency chain. Returning a CompletableFuture from thenApply produces a nested result instead. Neither method automatically moves a blocking database call to a suitable executor.
The sample supplies an executor explicitly and owns its shutdown. Non-async continuations may run on a thread that completes the previous stage; async forms without an executor normally use the common pool. Mixing slow blocking calls into an unrelated shared pool can delay other work. Choose pool boundaries to match task behaviour.
Failure recovery should preserve the policy
An exceptional completion skips ordinary value transformations until a recovery or observation stage handles it. exceptionally can substitute a result. Use that only when a fallback is valid: returning zero for a failed shipping rate lookup could accidentally turn a paid shipment into a free one.
The program separates a valid quote from a deliberately failed tracking lookup. The failed lookup maps to an explicit "unavailable" status. join reports failures through unchecked completion wrappers; get uses checked failure and interruption reporting. Deadline handling, cancellation of the underlying operation and cancellation of a future are separate issues. Read HTTP request policies before assuming a future timeout stops network work.
Working program
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ShipmentQuoteStages {
public static void main(String[] args) {
ExecutorService workers = Executors.newFixedThreadPool(2);
try {
CompletableFuture<Integer> rate = CompletableFuture.supplyAsync(() -> 120, workers);
CompletableFuture<Integer> insurance = CompletableFuture.supplyAsync(() -> 30, workers);
int total = rate.thenCombine(insurance, Integer::sum).join();
System.out.println("quote=" + total);
CompletableFuture<String> tracking = new CompletableFuture<>();
tracking.completeExceptionally(new IllegalStateException("Carrier unavailable"));
System.out.println(tracking.exceptionally(failure -> "unavailable").join());
} finally {
workers.shutdown();
}
}
}Output
quote=150
unavailableCost and failure boundaries
This graph has a fixed number of stages, so its bookkeeping storage is O(1). A chain created per incoming request grows with the number of admitted requests and can retain captured payloads until completion. Parallel tasks can overlap, but their external work and contention still determine end-to-end latency.
join blocks the caller in this sample, which is acceptable at its command-line boundary. Blocking a small worker pool while waiting for tasks submitted to the same saturated pool can starve progress. Prefer composition inside the graph and wait once at the boundary that actually requires the final answer.
Common Mistakes
- Do not use thenApply when the returned future needs flattening.
- Do not hide every failure behind a success-looking default.
- Do not assume cancelling a stage interrupts the underlying operation.
Connect the contracts
Compare the boundary explained in Executor lifecycle with the assumptions made by this program.
Continue with ownership and failure checks
Continue with Java CompletableFuture.allOf: completion barrier, not typed results, Java CompletableFuture handle versus whenComplete: recovery is explicit.
Continue with checked Spring boundaries
Continue with Spring @Async futures: make worker failure observable to the caller.
Continue with checked worker recovery
Continue with Spring async work has two failure points: submission and completion.
Related contract checks
Continue with Java CompletableFuture timeout: completion is not worker cancellation.
Continue with: Java HttpClient deadlines: separate connection and request timeouts, Java ForkJoinPool: own a custom pool and its shutdown.
Continue with: Java thenCompose: flatten a dependent asynchronous request, Java thenCombine: join two independent results without serializing them, Java applyToEither: use the first normal result of two stages, Java exceptionallyCompose: recover with another asynchronous stage.
