Concurrency

CompletableFuture

CompletableFuture<T> is a promise you can compose: chain transformations, combine independent results, race alternatives, and attach error handling — asynchronous workflows without callback pyramids or blocked threads.
  • Create: supplyAsync(supplier, executor); pass your own executor — the default is the common FJ pool
  • Chain: thenApply (map), thenCompose (flatMap), thenCombine (zip two)
  • Errors flow down the chain to exceptionally/handle — an unobserved failure vanishes silently
  • allOf/anyOf for fan-in; orTimeout/completeOnTimeout (Java 9) for deadlines
  • thenApply vs thenApplyAsync: same-thread continuation vs re-dispatch to a pool
  • With virtual threads, plain blocking code often replaces CF chains entirely
A composed pipeline
CompletableFuture<Quote> best =
    CompletableFuture.supplyAsync(() -> fetchUser(id), ioPool)
        .thenCompose(user -> CompletableFuture.supplyAsync(
                () -> fetchQuotes(user), ioPool))               // async dependency
        .thenApply(quotes -> quotes.stream()
                .min(comparing(Quote::price)).orElseThrow())     // pure transform
        .orTimeout(2, TimeUnit.SECONDS)
        .exceptionally(ex -> {
            log.warn("quote lookup failed", ex);
            return Quote.DEFAULT;                                // fallback
        });

Quote q = best.join();   // only the final consumer blocks (if anyone must)
Fan-out / fan-in
List<CompletableFuture<Price>> calls = vendors.stream()
        .map(v -> CompletableFuture.supplyAsync(() -> v.quote(item), ioPool))
        .toList();

CompletableFuture<List<Price>> all =
    CompletableFuture.allOf(calls.toArray(CompletableFuture[]::new))
        .thenApply(ignored -> calls.stream().map(CompletableFuture::join).toList());

Threading model: non-Async continuations run on whichever thread completed the previous stage (possibly your thread at composition time if it was already done); *Async variants re-dispatch to the pool. Keep continuations short and non-blocking, or make them Async on a suitable executor. And reassess with Virtual Threads: vthreadExecutor.submit(() -> { straightLineBlockingCode(); }) reads better than most CF chains and scales identically (Core Java and OCNJ both make this point).

Sources
  • Core Java, Volume I: Fundamentals (13th ed.)Ch. 10.7 — Asynchronous Computations
  • Optimizing Cloud Native Java (2nd ed.)Ch. 13 — Concurrent Performance Techniques