CompletableFuture

Async Pipelines

Future.get() blocks. CompletableFuture (Java 8) lets you describe what happens when the result arrives: transform it, chain another async call, combine two results, or recover from errors — without blocking threads.

MethodLikeUse
supplyAsync(supplier, executor)start async work that returns a value
thenApply(fn)maptransform the result
thenCompose(fn)flatMapchain another async call that returns a CompletableFuture
thenCombine(other, fn)zipcombine two independent results
thenAccept(consumer)forEachconsume the result
exceptionally(fn), handle(fn)catchrecover from errors
allOf(...), anyOf(...)wait for all / the first
orTimeout, completeOnTimeout (Java 9)timeouts
join()get()block for the result (unchecked exception)

Product Page Aggregation

Example

A product page needs details, price and reviews from three services. Details and price run in parallel, then reviews are chained.

import java.util.concurrent.*;

public class Main {
    static String sleepAndReturn(int ms, String value) {
        try { Thread.sleep(ms); } catch (InterruptedException e) { throw new RuntimeException(e); }
        return value;
    }

    public static void main(String[] args) {
        try (ExecutorService io = Executors.newFixedThreadPool(4)) {
            CompletableFuture<String> details = CompletableFuture.supplyAsync(() -> sleepAndReturn(150, "Redmi Note 13"), io);
            CompletableFuture<Integer> price = CompletableFuture.supplyAsync(() -> sleepAndReturn(100, "17999"), io)
                    .thenApply(Integer::parseInt);

            CompletableFuture<String> page = details
                    .thenCombine(price, (name, p) -> name + " | Rs " + p)
                    .thenCompose(header -> CompletableFuture.supplyAsync(
                            () -> header + " | rating " + sleepAndReturn(50, "4.3"), io));

            System.out.println(page.join());

            CompletableFuture<String> failing = CompletableFuture
                    .supplyAsync(() -> { if (true) throw new IllegalStateException("offers service down"); return "10% off"; }, io)
                    .exceptionally(ex -> "no offers (" + ex.getCause().getMessage() + ")");
            System.out.println(failing.join());

            CompletableFuture<String> slow = CompletableFuture.supplyAsync(() -> sleepAndReturn(2000, "late"), io)
                    .completeOnTimeout("default recommendations", 200, TimeUnit.MILLISECONDS);
            System.out.println(slow.join());
        }
    }
}

Output

Redmi Note 13 | Rs 17999 | rating 4.3
no offers (offers service down)
default recommendations

allOf

Example

Wait for many futures, then collect the results in order.

import java.util.*;
import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) {
        List<String> cities = List.of("Delhi", "Mumbai", "Chennai");
        List<CompletableFuture<String>> weather = cities.stream()
                .map(c -> CompletableFuture.supplyAsync(() -> c + ": " + (25 + c.length()) + "°C"))
                .toList();

        CompletableFuture.allOf(weather.toArray(new CompletableFuture[0])).join();
        weather.forEach(f -> System.out.println(f.join()));
    }
}

Output

Delhi: 30°C
Mumbai: 31°C
Chennai: 32°C

Tip

Tip: Without an executor argument, supplyAsync uses the common ForkJoinPool, which is sized for CPU work. For blocking I/O (HTTP, DB), pass your own executor — or in Java 21, a virtual-thread executor.