CompletableFuture in Java
CompletableFuture<T> is a Future you can build pipelines with. Instead of waiting with get(), you describe what should happen when a result arrives: transform it, start another task with it, combine it with another result, or handle a failure. Each step runs asynchronously, and nothing blocks until you finally ask for the answer. It was added in Java 8 and is the standard way to write non-blocking asynchronous code in Java. In this lesson, you will learn the main methods and build a parallel order processor with a timeout and a fallback.
Creating a CompletableFuture
| Method | Description |
|---|---|
supplyAsync(supplier) | It is used to run a task that returns a value on a pool (the common pool by default). |
supplyAsync(supplier, executor) | It runs it on the executor you choose. |
runAsync(runnable) | It runs a task with no result. |
completedFuture(value) | It creates an already-finished future. |
Chaining Methods
| Method | Description |
|---|---|
thenApply(fn) | It transforms the result (like map). |
thenAccept(consumer) | It uses the result and returns nothing. |
thenCompose(fn) | It starts another async step that depends on the result (like flatMap). |
thenCombine(other, fn) | It combines this result with another future's result. |
allOf(f1, f2, ...) | It completes when all futures complete. |
anyOf(f1, f2, ...) | It completes when the first one completes. |
exceptionally(fn) | It supplies a fallback value if a step failed. |
handle(fn) | It sees either the result or the exception. |
orTimeout(t, unit) / completeOnTimeout(v, t, unit) | It fails, or uses a default value, if too slow (Java 9). |
join() / get() | It waits for the final result. |
Example: A Simple Pipeline
Let's see an example that looks up a student, fetches the marks, and builds a report, step by step.
import java.util.concurrent.CompletableFuture;
public class Pipeline {
static int findMarks(String name) {
return switch (name) {
case "Navika" -> 91;
case "Srihitha" -> 78;
default -> 0;
};
}
public static void main(String[] args) {
CompletableFuture<String> report = CompletableFuture
.supplyAsync(() -> "Navika")
.thenApply(name -> name + ": " + findMarks(name))
.thenApply(String::toUpperCase);
System.out.println(report.join());
CompletableFuture<Integer> a = CompletableFuture.supplyAsync(() -> 91);
CompletableFuture<Integer> b = CompletableFuture.supplyAsync(() -> 78);
System.out.println("Total: " + a.thenCombine(b, Integer::sum).join());
CompletableFuture<Integer> broken = CompletableFuture
.supplyAsync(() -> Integer.parseInt("ninety"))
.exceptionally(e -> -1);
System.out.println("With fallback: " + broken.join());
}
}NAVIKA: 91
Total: 169
With fallback: -1Explanation: supplyAsync starts the first step on a pool thread. Each thenApply runs when the previous step finishes. thenCombine waits for two independent futures and adds their results. When parseInt fails, exceptionally replaces the failure with -1. join() waits for the final value (like get(), but it throws unchecked exceptions).
The Checkpoint: A Parallel Order Processor
This program processes three orders in parallel. Each order goes through three asynchronous steps: check stock, take payment, then ship. One payment service is very slow, so a timeout applies, and any failure falls back to a "pending" status instead of crashing the whole batch.
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class OrderProcessor {
record Order(int id, String customer, int amount) { }
static void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
static CompletableFuture<Order> checkStock(Order o, ExecutorService ex) {
return CompletableFuture.supplyAsync(() -> {
sleep(100);
if (o.amount() > 5000) {
throw new IllegalStateException("out of stock");
}
return o;
}, ex);
}
static CompletableFuture<String> pay(Order o, ExecutorService ex) {
return CompletableFuture.supplyAsync(() -> {
sleep(o.customer().equals("Advitha") ? 3000 : 100);
return "paid " + o.amount();
}, ex);
}
static CompletableFuture<String> process(Order o, ExecutorService ex) {
return checkStock(o, ex)
.thenCompose(order -> pay(order, ex))
.orTimeout(500, TimeUnit.MILLISECONDS)
.thenApply(payment -> "order " + o.id() + " for " + o.customer()
+ ": " + payment + ", shipped")
.exceptionally(e -> "order " + o.id() + " for " + o.customer()
+ ": pending (" + reason(e) + ")");
}
static String reason(Throwable e) {
Throwable cause = e.getCause() != null ? e.getCause() : e;
return cause instanceof java.util.concurrent.TimeoutException
? "payment timed out" : cause.getMessage();
}
public static void main(String[] args) {
List<Order> orders = List.of(
new Order(1, "Navika", 1200),
new Order(2, "Srihitha", 9000),
new Order(3, "Advitha", 800));
long start = System.nanoTime();
try (ExecutorService ex = Executors.newFixedThreadPool(6)) {
List<CompletableFuture<String>> results = orders.stream()
.map(o -> process(o, ex))
.toList();
CompletableFuture.allOf(results.toArray(CompletableFuture[]::new))
.join();
results.forEach(r -> System.out.println(r.join()));
ex.shutdownNow();
}
long ms = (System.nanoTime() - start) / 1_000_000;
System.out.println("Whole batch under one second? " + (ms < 1000));
}
}order 1 for Navika: paid 1200, shipped
order 2 for Srihitha: pending (out of stock)
order 3 for Advitha: pending (payment timed out)
Whole batch under one second? trueExplanation:
- All three orders start at once on the executor.
thenComposechains the payment step after the stock check, because payment needs the checked order.orTimeout(500 ms)fails Advitha's order when the slow payment service doesn't answer in time.exceptionallyturns any failure (out of stock, or a timeout) into a "pending" status, so one bad order doesn't stop the others.allOfwaits for every order;shutdownNowthen interrupts the slow payment still sleeping, so the batch takes about half a second instead of three.
Common Mistakes
Calling get() or join() after every step. That turns async code back into blocking code.
Using thenApply with a function that returns a CompletableFuture. You get a nested future; use thenCompose.
Forgetting exceptionally or handle. A failure silently ends the chain until someone calls join().
Interview Corner
What is CompletableFuture, and how does it differ from Future?
CompletableFuture implements both Future and CompletionStage. Unlike a plain Future, whose only way to get a result is the blocking get(), it lets you attach callbacks that run when the result is ready: thenApply to transform, thenCompose to chain another asynchronous step, thenCombine, allOf and anyOf to combine futures, and exceptionally or handle to recover from errors. It can also be completed manually and supports timeouts with orTimeout and completeOnTimeout.
Practice
- Easy: Use
supplyAsyncandthenApplyto fetch a name and print it in upper case withthenAccept. - Harder: Change the checkpoint so a timed-out payment is retried once with a 1-second timeout before being marked pending.
Key Takeaways
Was this tutorial helpful?