CompletableFuture (since Java 8) supports non-blocking asynchronous programming with composable pipelines for chaining, combining, and handling async results.

Creating CompletableFuture

  // Run async task with no return value
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    System.out.println("Running async");
});

// Supply a result asynchronously
CompletableFuture<String> supply = CompletableFuture.supplyAsync(() -> {
    return fetchDataFromApi();
});

// Already completed
CompletableFuture<String> completed = CompletableFuture.completedFuture("done");
  

By default, runAsync and supplyAsync use the ForkJoinPool.commonPool(). Pass a custom executor for production:

  ExecutorService executor = Executors.newFixedThreadPool(4);
CompletableFuture.supplyAsync(() -> fetchData(), executor);
  

Chaining Operations

  CompletableFuture.supplyAsync(() -> "  hello  ")
    .thenApply(String::trim)
    .thenApply(String::toUpperCase)
    .thenAccept(result -> System.out.println(result)) // HELLO
    .join(); // wait for completion
  
Method Input Output
thenApply(fn) T U
thenAccept(consumer) T Void
thenRun(runnable) Void
thenCompose(fn) T CompletableFuture<U>
thenCombine(other, fn) T, U V

thenApply vs thenCompose

  // thenApply — flatten one level
CompletableFuture<Integer> length = CompletableFuture
    .supplyAsync(() -> "hello")
    .thenApply(String::length); // CompletableFuture<Integer>

// thenCompose — flatten nested futures
CompletableFuture<User> user = CompletableFuture
    .supplyAsync(() -> "[email protected]")
    .thenCompose(email -> findUserByEmail(email)); // returns CompletableFuture<User>
  

Combining Futures

  CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");

// Wait for both
CompletableFuture<String> combined = future1.thenCombine(future2, (a, b) -> a + " " + b);

// Wait for all
CompletableFuture<Void> all = CompletableFuture.allOf(future1, future2);
all.join();

// Wait for any
CompletableFuture<Object> any = CompletableFuture.anyOf(future1, future2);
  

Exception Handling

  CompletableFuture.supplyAsync(() -> {
    if (Math.random() > 0.5) throw new RuntimeException("Failed");
    return "Success";
})
.exceptionally(ex -> {
    System.out.println("Error: " + ex.getMessage());
    return "Fallback";
})
.handle((result, ex) -> {
    if (ex != null) return "Recovered";
    return result;
});
  

Getting Results

  // Blocking
String result = future.get();                    // waits indefinitely
String result2 = future.get(5, TimeUnit.SECONDS); // with timeout

// Non-blocking check
if (future.isDone()) {
    String r = future.getNow("default");
}

// join() — like get() but wraps in unchecked CompletionException
String r = future.join();
  

Real-World Example

  public CompletableFuture<OrderSummary> getOrderSummary(Long orderId) {
    CompletableFuture<Order> orderFuture =
        CompletableFuture.supplyAsync(() -> orderService.findById(orderId));

    CompletableFuture<List<Item>> itemsFuture =
        CompletableFuture.supplyAsync(() -> itemService.findByOrderId(orderId));

    CompletableFuture<User> userFuture = orderFuture
        .thenCompose(order -> userService.findByIdAsync(order.getUserId()));

    return orderFuture
        .thenCombine(itemsFuture, (order, items) -> order)
        .thenCombine(userFuture, (order, user) ->
            new OrderSummary(order, itemsFuture.join(), user));
}
  

Best Practices

  • Always use a dedicated ExecutorService in production — do not rely on the common pool
  • Handle exceptions with exceptionally or handle
  • Avoid blocking calls (get()) inside async pipelines
  • Use thenCompose (not thenApply) when the next step returns a CompletableFuture
  • Shut down custom executors when the application stops