Clean • Professional
In modern microservices architecture, a single request often depends on multiple downstream services. If those calls are executed sequentially, overall latency increases significantly.
CompletableFuture in Java allows you to execute independent service calls in parallel, improving response time and throughput. It integrates seamlessly with Spring Boot applications.
CompletableFuture is a powerful class from Java’s java.util.concurrent package (introduced in Java 8) that helps you write asynchronous and non-blocking code in a clean and structured way.
It allows you to:
Simple Definition
CompletableFuture is a modern Java feature that enables asynchronous, non-blocking, and composable programming.
In microservices architecture, one API call often depends on multiple services.
Example flow:

When services are called one by one, each service waits for the previous one to finish.
Total Response Time = Sum of All Services
Example:
Total = 900ms → Client waits for all services to complete, increasing API latency.
With CompletableFuture, services run simultaneously instead of sequentially.
Total Response Time ≈ Slowest Service
Example:
Total ≈ 400ms → Significantly faster than sequential calls.
Enable asynchronous processing by adding @EnableAsync in a configuration class:
@Configuration
@EnableAsync
public class AsyncConfig {
}
Use @Async with CompletableFuture to execute multiple service calls in parallel:
@Service
public class OrderAggregatorService {
@Async
public CompletableFuture<User> getUser(Long userId) {
return CompletableFuture.completedFuture(userService.getUser(userId));
}
@Async
public CompletableFuture<Payment> getPayment(Long orderId) {
return CompletableFuture.completedFuture(paymentService.getPayment(orderId));
}
@Async
public CompletableFuture<Inventory> getInventory(Long productId) {
return CompletableFuture.completedFuture(inventoryService.getInventory(productId));
}
}
Each service call runs in a separate thread, improving performance for multiple dependent services.
Combine all async calls in the controller and wait for completion:
@GetMapping("/order-summary/{id}")
public OrderSummary getOrderSummary(@PathVariable Long id) throws Exception {
CompletableFuture<User> userFuture = service.getUser(id);
CompletableFuture<Payment> paymentFuture = service.getPayment(id);
CompletableFuture<Inventory> inventoryFuture = service.getInventory(id);
// Wait for all futures to complete
CompletableFuture.allOf(userFuture, paymentFuture, inventoryFuture).join();
return new OrderSummary(
userFuture.get(),
paymentFuture.get(),
inventoryFuture.get()
);
}
Now all services execute in parallel.
1. allOf() – Wait for All Tasks
CompletableFuture.allOf(f1, f2, f3).join();
Used when all results are required.
2. thenApply() – Transform Result
future.thenApply(result -> result.toUpperCase());
3. thenCombine() – Combine Two Futures
future1.thenCombine(future2, (r1, r2) -> r1 + r2);
4. thenCompose() – Chain Async Calls
future1.thenCompose(result ->
CompletableFuture.supplyAsync(() -> anotherCall(result)));
5. exceptionally() – Error Handling
future.exceptionally(ex -> {
log.error("Error occurred", ex);
return fallbackValue;
});
Using the default ForkJoinPool in high-load microservices can lead to performance bottlenecks. Always configure a dedicated thread pool for async tasks.
@Bean(name = "asyncExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // Minimum threads always available
executor.setMaxPoolSize(20); // Maximum threads under high load
executor.setQueueCapacity(100); // Tasks waiting if all threads busy
executor.setThreadNamePrefix("CF-"); // Thread name prefix for easier debugging
executor.initialize();
return executor;
}
Usage:
CompletableFuture.supplyAsync(() -> serviceCall(), asyncExecutor);
serviceCall() asynchronously using the configured thread pool.CompletableFuture is widely used in microservices and high-performance APIs to execute multiple tasks in parallel without blocking the main thread. Common scenarios include:
In microservices, downstream services can:
To handle this gracefully, combine CompletableFuture with:
Example: Fallback on Failure
CompletableFuture<User> userFuture =
CompletableFuture.supplyAsync(() -> userService.getUser(id))
.exceptionally(ex ->newUser("default"));
userService fails, a default User object is returned.| Feature | CompletableFuture | @Async |
|---|---|---|
| Background execution | Runs tasks asynchronously in a separate thread | Runs tasks asynchronously using Spring’s thread pool |
| Parallel composition | Easily combine multiple async tasks in parallel | Limited support for combining multiple tasks |
| Combining results | Built-in methods like thenCombine and allOf | Must manually handle result aggregation |
| Error handling | Advanced, supports exceptionally(), handle() | Basic, mostly logs uncaught exceptions |
| Best For | Service aggregation, parallel API calls, complex workflows | Simple fire-and-forget background tasks |
Avoid CompletableFuture if your tasks require:
For such cases, use messaging systems like:
.get()CompletableFuture is a powerful concurrency tool for microservice-based systems. It enables: