CompletableFuture in Java

Naveen AnamaneniUpdated · 6 min read

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

MethodDescription
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

MethodDescription
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.

Pipeline.java▶ Try it
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());
    }
}
Output
NAVIKA: 91
Total: 169
With fallback: -1

Explanation: 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.

OrderProcessor.java▶ Try it
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));
    }
}
Output
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? true

Explanation:

  • All three orders start at once on the executor.
  • thenCompose chains 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.
  • exceptionally turns any failure (out of stock, or a timeout) into a "pending" status, so one bad order doesn't stop the others.
  • allOf waits for every order; shutdownNow then 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

  1. Easy: Use supplyAsync and thenApply to fetch a name and print it in upper case with thenAccept.
  2. Harder: Change the checkpoint so a timed-out payment is retried once with a 1-second timeout before being marked pending.

Quick check: CompletableFuture

4 questions
1Which method transforms a result?
2Which method chains a step that itself returns a CompletableFuture?
3Which method supplies a fallback value after a failure?
4Which pool does supplyAsync use without an executor?

Key Takeaways

Was this tutorial helpful?

Lesson 417 of 482