Back to results for “Error Handling”
Java › Concurrency & Threads

How do CompletableFuture's composition methods (thenApply, thenCompose, thenCombine) enable building asynchronous pipelines, and what's the difference between thenApply and thenCompose?

Advanced

Answer

CompletableFuture represents a value that will be available asynchronously in the future, with fluent composition methods letting you chain dependent asynchronous operations without manually blocking and coordinating threads yourself -- thenApply(Function) transforms the eventual result synchronously within the same stage (for a simple, non-async transformation), while thenCompose(Function returning another CompletableFuture) is used specifically when your transformation itself returns another asynchronous operation, flattening what would otherwise become a nested CompletableFuture<CompletableFuture<T>> into a single CompletableFuture<T> (analogous to flatMap versus map in Streams/Optional), and thenCombine merges two independent CompletableFutures once both complete.
CompletableFuture<User> userFuture = fetchUserAsync(userId);

// thenApply: simple synchronous transformation of the result
CompletableFuture<String> nameFuture = userFuture.thenApply(User::getName);

// thenCompose: chaining another ASYNC operation, avoiding nested CompletableFuture<CompletableFuture<T>>
CompletableFuture<List<Order>> ordersFuture = userFuture.thenCompose(user -> fetchOrdersAsync(user.getId()));

// thenCombine: merge two independent async operations once both complete
CompletableFuture<Report> reportFuture = userFuture.thenCombine(fetchInventoryAsync(), (user, inventory) -> buildReport(user, inventory));
Real-world example A microservice orchestrating calls to three downstream services (user info, order history, and inventory) uses thenCompose to sequentially chain dependent async calls (fetching orders requires the user ID from the first call) and thenCombine to run two independent calls concurrently, composing the entire multi-service workflow declaratively without any manual thread coordination.

Common follow-ups: What happens to exceptions thrown partway through a CompletableFuture chain, and how does exceptionally()/handle() address this?;What thread does each stage of a CompletableFuture chain actually execute on by default?