Blocking a thread to wait for a result wastes a resource that could be doing
other work. CompletableFuture models a value that will arrive later and lets
you chain transformations without ever calling get() prematurely.
Compose instead of block
Each stage runs when the previous one completes. Nothing blocks the calling thread until you explicitly join at the very end.
CompletableFuture<Order> future = CompletableFuture
.supplyAsync(() -> loadUser(userId), executor)
.thenApply(this::validate)
.thenCompose(user -> CompletableFuture.supplyAsync(() -> createOrder(user)))
.exceptionally(ex -> {
log.error("order failed", ex);
return Order.empty();
});Combine parallel work
When two calls are independent, fire both and merge the results with
thenCombine. The total latency becomes the slower of the two, not the sum.
var price = CompletableFuture.supplyAsync(() -> pricing.quote(sku));
var stock = CompletableFuture.supplyAsync(() -> inventory.check(sku));
price.thenCombine(stock, (p, s) -> new Availability(p, s)).join();Takeaways
- Prefer
thenComposefor chaining futures andthenApplyfor plain values. - Always supply your own
Executorinstead of the common pool for I/O work. - Handle failure with
exceptionallyorhandleso errors do not vanish.
Never mix blocking get() calls inside a chain — it defeats the entire purpose
and can deadlock a bounded pool.