Home/Learn/Java A–Z/CompletableFuture — Advanced Patterns

CompletableFuture — Advanced Patterns

Advanced
Concurrency

Advanced CompletableFuture patterns — custom executors, timeout handling, retry logic, bulkhead isolation, and combining complex async pipelines.

Overview

Beyond the basics, CompletableFuture enables sophisticated async patterns: retry with exponential backoff, circuit breakers, bulkhead isolation (separate thread pools per service), fan-out/fan-in with partial results, and graceful degradation. Java 9 added orTimeout() and completeOnTimeout() for deadline handling. Understanding when to complete a future exceptionally vs. completing with a fallback is key for resilient async code.

Custom Executors and Bulkhead Pattern

The default ForkJoinPool.commonPool() is shared by all parallel streams, CompletableFuture.supplyAsync() calls, and other framework code. Saturating it with slow I/O tasks blocks the whole application.

The bulkhead pattern isolates work into separate thread pools — a slow dependency can only exhaust its own pool, not affect others.

Bulkhead.java
// Separate executors per external dependency (bulkhead)
ExecutorService dbPool      = Executors.newFixedThreadPool(20);
ExecutorService paymentPool = Executors.newFixedThreadPool(5);
ExecutorService emailPool   = Executors.newVirtualThreadPerTaskExecutor();

public CompletableFuture<OrderResult> placeOrderAsync(OrderRequest req) {
    CompletableFuture<User>    userFuture =
        CompletableFuture.supplyAsync(() -> userDb.find(req.userId()), dbPool);

    CompletableFuture<Payment> payFuture =
        CompletableFuture.supplyAsync(() -> payment.charge(req), paymentPool);

    return userFuture.thenCombineAsync(payFuture, (user, pay) -> {
        Order order = orderDb.save(new Order(user, pay));
        // Email on separate pool — doesn't block order completion
        CompletableFuture.runAsync(
            () -> emailService.sendConfirmation(user, order), emailPool);
        return new OrderResult(order.id(), pay.txnId());
    }, dbPool); // combine result on DB pool
}

Retry with Exponential Backoff

Retry logic wraps an async operation: on failure, schedule a retry after a delay (exponential backoff with jitter avoids thundering herd). CompletableFuture.delayedExecutor() (Java 9+) schedules execution after a delay without blocking a thread.

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

public static <T> CompletableFuture<T> withRetry(
        Supplier<CompletableFuture<T>> operation,
        int maxAttempts,
        Duration initialDelay,
        ScheduledExecutorService scheduler) {

    CompletableFuture<T> result = new CompletableFuture<>();
    attempt(operation, maxAttempts, initialDelay, scheduler, result, 1);
    return result;
}

private static <T> void attempt(
        Supplier<CompletableFuture<T>> operation, int maxAttempts,
        Duration delay, ScheduledExecutorService scheduler,
        CompletableFuture<T> result, int attempt) {

    operation.get().whenComplete((value, ex) -> {
        if (ex == null) {
            result.complete(value);
        } else if (attempt >= maxAttempts) {
            result.completeExceptionally(ex);
        } else {
            // Exponential backoff with jitter
            long millis = delay.toMillis() * (1L << (attempt - 1));
            long jitter  = ThreadLocalRandom.current().nextLong(millis / 2);
            scheduler.schedule(
                () -> attempt(operation, maxAttempts, delay, scheduler, result, attempt + 1),
                millis + jitter, TimeUnit.MILLISECONDS);
        }
    });
}

// Usage
withRetry(() -> httpClient.sendAsync(request, ofString()), 3,
    Duration.ofMillis(200), scheduler)
    .thenAccept(resp -> System.out.println("Got: " + resp.body()));

Fan-Out / Fan-In with Partial Results

Fan-out: dispatch N parallel async calls. Fan-in: collect results. With allOf(), you wait for all — but one slow/failing request blocks everything. For resilient fan-in, collect partial results even when some futures fail.

FanOutFanIn.java
// Fan-out to multiple services
List<String> serviceUrls = List.of(url1, url2, url3, url4, url5);

List<CompletableFuture<String>> futures = serviceUrls.stream()
    .map(url -> CompletableFuture
        .supplyAsync(() -> callService(url), ioPool)
        .orTimeout(2, TimeUnit.SECONDS)           // per-request timeout
        .exceptionally(ex -> null))               // failures → null
    .collect(Collectors.toList());

// Fan-in: collect all results (including nulls for failures)
CompletableFuture<List<String>> allResults = CompletableFuture
    .allOf(futures.toArray(new CompletableFuture[0]))
    .thenApply(v -> futures.stream()
        .map(CompletableFuture::join)             // all done — join is safe
        .filter(Objects::nonNull)                 // drop failed requests
        .collect(Collectors.toList()));

// Result: successful responses only, within 2s deadline
List<String> results = allResults
    .orTimeout(3, TimeUnit.SECONDS)               // overall timeout
    .join();

System.out.println("Got " + results.size() + "/" + serviceUrls.size() + " results");

Key Points to Remember

  • Use separate thread pools per external dependency (bulkhead) to prevent cascade failures.
  • completeOnTimeout() provides a fallback value on timeout; orTimeout() completes exceptionally.
  • delayedExecutor() schedules future execution without blocking a thread.
  • Retry with exponential backoff + jitter prevents thundering herd on service recovery.
  • Fan-in with partial results: use exceptionally(ex -> null) to collect successes only.

Practice CompletableFuture — Advanced Patterns in the Playground

Run and modify code directly in your browser - no setup needed.

Interview Questions

Sign in to ask Aria
1

What is the bulkhead pattern and how do you implement it with CompletableFuture?

HardNetflix
2

What is the difference between orTimeout() and completeOnTimeout()?

MediumAmazon
3

How would you implement retry with exponential backoff using CompletableFuture?

HardGoogle
4

How do you collect partial results from a fan-out when some futures fail?

HardMicrosoft
5

Why is it dangerous to use ForkJoinPool.commonPool() for I/O-heavy async tasks?

MediumOracle

Ask Aria about CompletableFuture — Advanced Patterns

Your personal AI tutor — ask anything about this concept