Stage 8: Concurrency and the JVM, lesson 4 of 7

CompletableFuture

Advanced3 min read@since 9Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

CompletableFuture (Java 8) represents an asynchronous result that you can compose without blocking.

  • Start: supplyAsync(supplier, executor), runAsync
  • Transform: thenApply (like map), thenCompose (like flatMap, for dependent async calls)
  • Combine: thenCombine (two futures), allOf, anyOf
  • Errors: exceptionally, handle, whenComplete
  • Timeouts (Java 9): orTimeout, completeOnTimeout

It's ideal for calling several services in parallel and merging the results.

Example

Java
ExecutorService io = Executors.newFixedThreadPool(8);

CompletableFuture<User> user =
    CompletableFuture.supplyAsync(() -> userApi.get(id), io);
CompletableFuture<List<Course>> courses =
    CompletableFuture.supplyAsync(() -> courseApi.forUser(id), io);

CompletableFuture<Dashboard> dashboard = user
    .thenCombine(courses, Dashboard::new)
    .orTimeout(2, TimeUnit.SECONDS)                  // Java 9+
    .exceptionally(ex -> Dashboard.fallback(id));

Dashboard d = dashboard.join();

Common mistake

Using thenApply when your function itself returns a CompletableFuture. You end up with CompletableFuture<CompletableFuture<T>>; use thenCompose instead.

Under the hood

Without an explicit executor, the async methods use ForkJoinPool.commonPool(), which the whole JVM shares; blocking I/O there starves parallel streams and other tasks. With virtual threads (Java 21), plain blocking code in Executors.newVirtualThreadPerTaskExecutor() is often clearer than long CompletableFuture chains.

Check yourself

Which method waits for all of several futures?

How this connects

Where this leads

You've reached the end of this thread. Try a learning path for what's next.

Part of Crack the Java interview, Microservices and production.

Was this lesson helpful?

Finished reading? Mark it complete to track your progress.