CompletableFuture
CompletableFuture — Java's async programming model for composing non-blocking operations and handling results and errors.
Last reviewed: January 2025
🔍 What Is It?
CompletableFuture API:
Creating:
- →
supplyAsync(Supplier)— async, returns value - →
runAsync(Runnable)— async, no return value - →
completedFuture(value)— already completed with value
Transforming result:
- →
thenApply(Function)— transform result (like Stream.map) - →
thenCompose(Function<T, CompletableFuture<U>>)— chain async operations (like flatMap) - →
thenAccept(Consumer)— consume result, no return - →
thenRun(Runnable)— run after completion, ignores result
Combining:
- →
thenCombine(other, BiFunction)— combine two futures when both complete - →
allOf(futures...)— wait for all to complete - →
anyOf(futures...)— complete when first finishes
Exception handling:
- →
exceptionally(Function<Throwable, T>)— recover from exception - →
handle(BiFunction<T, Throwable, U>)— handle both result and exception - →
whenComplete(BiConsumer)— side effect on completion (success or failure)
Async variants:
- →
thenApplyAsync,thenComposeAsyncetc. — run callback in executor (not inline)
🎯 Why Do We Need It?
CompletableFuture solves the "callback hell" of raw threads and the blocking of Future.get():
// Old Future: blocking
Future<User> userFuture = executor.submit(() -> fetchUser(id));
Future<Orders> ordersFuture = executor.submit(() -> fetchOrders(id));
User user = userFuture.get(); // blocks!
Orders orders = ordersFuture.get(); // blocks!
// CompletableFuture: non-blocking composition
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> fetchUser(id));
CompletableFuture<Orders> ordersFuture = CompletableFuture.supplyAsync(() -> fetchOrders(id));
CompletableFuture.allOf(userFuture, ordersFuture)
.thenRun(() -> {
User user = userFuture.join();
Orders orders = ordersFuture.join();
buildResponse(user, orders);
});🏗️ What Happens in Real Projects?
Common in enterprise microservices:
- →Parallel API calls to multiple services
- →Async database operations
- →Spring @Async methods returning CompletableFuture
- →Non-blocking HTTP clients (WebClient)
⚙️ How It Works
Execution model:
By default, CompletableFuture.supplyAsync() uses ForkJoinPool.commonPool(). For production, always provide your own executor to control thread count and naming.
thenApply vs thenApplyAsync:
- →thenApply: callback runs in the same thread that completed the future (or the calling thread if already complete)
- →thenApplyAsync: callback runs in the executor (ForkJoinPool or provided executor)
- →For I/O operations in callbacks, use Async variants to avoid blocking the completing thread
💻 Example
CompletableFuture in Enterprise Code — Async patterns for microservice communication and parallel operations.
🤔 Common Confusions
❓ What is the difference between thenApply and thenCompose?
thenApply is like Stream.map() — it transforms the result to another value: CompletableFuture<T>.thenApply(T -> U) returns CompletableFuture<U>. thenCompose is like Stream.flatMap() — it chains to another async operation: CompletableFuture<T>.thenCompose(T -> CompletableFuture<U>) returns CompletableFuture<U> (not CompletableFuture<CompletableFuture<U>>). Use thenCompose when the transformation itself is async.
❓ What is the difference between join() and get()?
Both block until the future completes. get() throws checked exceptions (InterruptedException, ExecutionException). join() throws unchecked exceptions (CompletionException). In lambda expressions (which cannot throw checked exceptions), use join(). In regular code where you want to handle InterruptedException, use get().
🚨 Common Production Issues
Using ForkJoinPool.commonPool() for I/O operations — the common pool has limited threads. I/O operations block these threads, starving parallel streams. Always provide a custom executor for I/O-bound CompletableFutures.
Exception swallowing — if you do not attach an exceptionally() or handle() handler, exceptions are silently swallowed. Always handle exceptions in CompletableFuture pipelines.
Blocking in thenApply — thenApply runs in the completing thread. If you do I/O in thenApply, you block that thread. Use thenApplyAsync with an executor for I/O operations.
✅ Best Practices
Always provide a custom executor for production CompletableFutures.
Always handle exceptions with exceptionally() or handle().
Use thenApplyAsync for I/O operations in callbacks.
Use orTimeout() (Java 9+) to prevent indefinitely hanging futures.
Use CompletableFuture.allOf() for parallel execution, not sequential chaining.
Return CompletableFuture from @Async methods for proper async handling.
🏛️ Architect's Note
CompletableFuture is the right tool for orchestrating multiple async operations in a service. However, for reactive streams with backpressure (handling millions of events), use Project Reactor (Spring WebFlux) or RxJava. With Java 21 virtual threads, much of the complexity of CompletableFuture can be avoided — simple blocking code with virtual threads achieves similar concurrency with less complexity.
💬 Frequently Asked Questions
🎯 Key Takeaways
🔗 Related Topics
🎯 Practice this topic in interviews
Core Java Interview Prep
Practice interview questions that connect directly to this technology track.
↗ Where this topic fits
Core Java Technology Guide
Return to the Core Java learning hub.
Backend Engineer Career Path
A structured journey through Core Java, Spring Boot, production backend engineering, and interview preparation.
Full Stack Java Developer Career Path
A guided route through frontend foundations, Java backend engineering, delivery workflows, and interview readiness.
Sources
Sources / References
Use these Core Java references to verify the concept in official docs and continue into the most relevant SplashRide pages.
Official documentation
Related SplashRide pages
SplashRide Core Java learning path
Browse the full Core Java topic collection and learning order on SplashRide.
Core Java Interview Prep
Practice Core Java interview questions and scenario-based answers.
Backend Engineer Career Path
A structured journey through Core Java, Spring Boot, production backend engineering, and interview preparation.
Full Stack Java Developer Career Path
A guided route through frontend foundations, Java backend engineering, delivery workflows, and interview readiness.
Threads
Java threads — creating and managing threads, thread lifecycle, and the concurrency problems every enterprise developer must understand.
Runnable
The Runnable interface — the standard way to define a task for concurrent execution in Java.