CompletableFuture enables non-blocking async programming in Java. Learn how to use it in Spring Boot for parallel calls, timeout handling, and error recovery - with real examples.
JOptimize Team
CompletableFuture is Java's primary tool for composing asynchronous operations - parallel HTTP calls, non-blocking I/O, timeout-bounded tasks. Used correctly, it dramatically improves throughput. Used incorrectly, it silently swallows exceptions, blocks threads, and creates race conditions that are nearly impossible to debug.
// Run a task asynchronously CompletableFuture<User> future = CompletableFuture.supplyAsync(() -> { return userRepository.findById(userId); // Runs on ForkJoinPool.commonPool() }); // Transform the result CompletableFuture<UserDto> dtoFuture = future .thenApply(user -> UserMapper.toDto(user)); // Get the result (blocks current thread) UserDto dto = dtoFuture.get(5, TimeUnit.SECONDS);
Critical: Always use a named executor, never the default ForkJoinPool.commonPool():
@Configuration public class AsyncConfig { @Bean("ioExecutor") public ExecutorService ioExecutor() { return Executors.newVirtualThreadPerTaskExecutor(); // Java 21+ // Or: new ThreadPoolExecutor(10, 50, 60, SECONDS, new ArrayBlockingQueue<>(100)) } } // Pass your executor to every supplyAsync: CompletableFuture.supplyAsync(() -> callExternalApi(), ioExecutor);
The common pool is shared across your JVM. One blocking call starves other futures.
@Service @RequiredArgsConstructor public class ProductPageService { private final ProductService productService; private final ReviewService reviewService; private final InventoryService inventoryService; private final ExecutorService ioExecutor; public ProductPageDto getProductPage(Long productId) { // Launch all three in parallel CompletableFuture<Product> productFuture = CompletableFuture.supplyAsync( () -> productService.findById(productId), ioExecutor); CompletableFuture<List<Review>> reviewsFuture = CompletableFuture.supplyAsync( () -> reviewService.findByProduct(productId), ioExecutor); CompletableFuture<Integer> stockFuture = CompletableFuture.supplyAsync( () -> inventoryService.getStock(productId), ioExecutor); // Wait for all three CompletableFuture.allOf(productFuture, reviewsFuture, stockFuture).join(); return new ProductPageDto( productFuture.join(), reviewsFuture.join(), stockFuture.join() ); // Total time = max(product, reviews, stock) instead of sum } }
If product=100ms, reviews=200ms, stock=150ms - sequential takes 450ms, parallel takes 200ms.
// completeOnTimeout: return fallback after timeout (Java 9+) CompletableFuture<List<Review>> reviewsFuture = CompletableFuture.supplyAsync(() -> reviewService.findByProduct(productId), ioExecutor) .completeOnTimeout(List.of(), 300, TimeUnit.MILLISECONDS); // Empty list if slow // orTimeout: throw TimeoutException after timeout (Java 9+) CompletableFuture<Product> productFuture = CompletableFuture.supplyAsync(() -> productService.findById(productId), ioExecutor) .orTimeout(500, TimeUnit.MILLISECONDS); // Fail fast
// exceptionally: recover from any exception CompletableFuture<List<Review>> reviewsFuture = CompletableFuture.supplyAsync(() -> reviewService.findByProduct(productId), ioExecutor) .exceptionally(ex -> { log.warn("Reviews unavailable for product {}: {}", productId, ex.getMessage()); return List.of(); // Graceful degradation }); // handle: process both success and failure CompletableFuture<ProductDto> result = CompletableFuture.supplyAsync(() -> productService.findById(productId), ioExecutor) .handle((product, ex) -> { if (ex != null) { log.error("Product load failed", ex); throw new ProductNotFoundException(productId); } return ProductMapper.toDto(product); }); // whenComplete: side effects without modifying result .whenComplete((result, ex) -> { if (ex != null) metricsService.recordFailure("product-load"); else metricsService.recordSuccess("product-load"); });
// thenApply: transform result synchronously CompletableFuture<String> name = userFuture .thenApply(user -> user.getFirstName() + " " + user.getLastName()); // thenCompose: chain async operations (flatMap) CompletableFuture<List<Order>> orders = userFuture .thenCompose(user -> CompletableFuture.supplyAsync( () -> orderService.findByUser(user.getId()), ioExecutor )); // thenCombine: combine two independent futures CompletableFuture<OrderSummary> summary = productFuture .thenCombine(priceFuture, (product, price) -> new OrderSummary(product, price));
@Service public class EmailService { @Async("ioExecutor") public CompletableFuture<Void> sendWelcomeEmail(User user) { emailClient.send(new WelcomeEmail(user)); return CompletableFuture.completedFuture(null); } } // Caller: CompletableFuture<Void> emailFuture = emailService.sendWelcomeEmail(user); // Don't block - fire and forget, or handle the future emailFuture.exceptionally(ex -> { log.error("Email send failed for user {}", user.getId(), ex); return null; });
CompletableFuture is silently swallowed unless you call .get() or chain .exceptionally()ForkJoinPool.commonPool() for blocking I/O - this pool is designed for CPU-bound work; use a dedicated ExecutorService with virtual threads for I/O.get() without timeout - this blocks the calling thread indefinitely; always use .get(timeout, unit) or .orTimeout()CompletableFuture - calling Thread.sleep() or another .get() inside a supplyAsync() lambda defeats the purpose and can deadlockCompletableFuture enables parallel execution that reduces total response time to the slowest call rather than the sum of all calls. Always use a dedicated executor (virtual threads in Java 21), handle timeouts with completeOnTimeout() or orTimeout(), chain error recovery with exceptionally(), and never let exceptions propagate silently. The pattern of launching parallel tasks and collecting results with allOf().join() is the most valuable use case for most Spring Boot services.
JOptimize flags unhandled CompletableFuture exceptions, blocking calls inside async lambdas, and missing timeout configurations in your Spring Boot project.
Write safer async Java code - free scan, no configuration required.
Master Spring Boot, security, and Java performance with hands-on courses.
JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.