Concurrency - CompletableFuture
Learn asynchronous pipelines in Java with CompletableFuture, composition patterns, and robust error handling.
#java #concurrency #completablefuture #async
Why this step matters
Many backend flows call multiple services and should not block a request thread for each step.
CompletableFuture enables non-blocking composition and better pipeline structure.
Basic async task
CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() -> fetchUserName(42));
String name = userFuture.join();
join() waits and wraps errors in unchecked exceptions.
Transform and chain
CompletableFuture<String> greeting = CompletableFuture
.supplyAsync(() -> "briac")
.thenApply(String::toUpperCase)
.thenApply(name -> "hello " + name);
thenApply: synchronous transform of previous resultthenCompose: chain another async future (flatten nested futures)
Compose multiple async tasks
CompletableFuture<User> user = CompletableFuture.supplyAsync(() -> loadUser(1));
CompletableFuture<List<Order>> orders = CompletableFuture.supplyAsync(() -> loadOrders(1));
CompletableFuture<UserDashboard> dashboard = user.thenCombine(
orders,
UserDashboard::new
);
Error handling
CompletableFuture<String> safe = CompletableFuture
.supplyAsync(this::callRemote)
.exceptionally(ex -> "fallback");
Useful handlers:
exceptionally(...)handle(...)whenComplete(...)
Timeouts
CompletableFuture<String> result = CompletableFuture
.supplyAsync(this::callRemote)
.orTimeout(2, TimeUnit.SECONDS)
.exceptionally(ex -> "timeout-fallback");
Common mistakes
- blocking too early with
join()everywhere - mixing heavy blocking I/O in default common pool
- forgetting error paths in composed pipelines
- creating overly complex chains without naming intermediate steps
Takeaway
- Use
CompletableFuturefor async composition, not just async execution - Prefer
thenCompose/thenCombinefor clear pipelines - Handle timeouts and failures explicitly
- Keep async chains readable and observable