CompletableFuture

September 24, 201924 min readUpdated 2/28/2026

1. What is CompletableFuture?

Think of ordering coffee at a busy cafe. You place your order, get a receipt with a number, and step aside. You are free to check your phone, chat with a friend, or grab a seat. When your coffee is ready, the barista calls your number. You did not stand at the counter blocking everyone behind you — you continued living your life while the work happened in the background.

CompletableFuture is that receipt. It is a promise that a value will be available at some point in the future. Introduced in Java 8 as part of java.util.concurrent, it is the most powerful tool Java provides for writing asynchronous, non-blocking code.

The Problem with Future

Java 5 introduced Future<T>, which represents an asynchronous computation. But Future has a fatal flaw: the only way to get the result is to call get(), which blocks the calling thread. You cannot attach a callback. You cannot chain operations. You cannot combine multiple futures. Calling get() defeats the purpose of async programming.

What CompletableFuture Adds

Feature Future CompletableFuture
Get result get() — blocks get(), join(), or non-blocking callbacks
Attach callback Not supported thenApply(), thenAccept(), thenRun()
Chain operations Not supported thenCompose(), thenApply()
Combine futures Not supported thenCombine(), allOf(), anyOf()
Exception handling ExecutionException wrapper exceptionally(), handle(), whenComplete()
Manually complete Not supported complete(), completeExceptionally()
import java.util.concurrent.*;

public class FutureVsCompletableFuture {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // OLD WAY: Future -- blocks on get()
        System.out.println("=== Future (blocking) ===");
        Future<String> future = executor.submit(() -> {
            Thread.sleep(500);
            return "Data from database";
        });
        // This line BLOCKS until the result is ready
        String result = future.get();
        System.out.println("Got: " + result);

        // NEW WAY: CompletableFuture -- non-blocking
        System.out.println("\n=== CompletableFuture (non-blocking) ===");
        CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
            try { Thread.sleep(500); } catch (InterruptedException e) { throw new RuntimeException(e); }
            return "Data from database";
        });

        // Attach a callback -- runs when data is ready, does NOT block
        cf.thenApply(data -> data.toUpperCase())
          .thenAccept(data -> System.out.println("Got: " + data));

        System.out.println("Main thread is free!");
        Thread.sleep(1000);
        executor.shutdown();

        // Output:
        // === Future (blocking) ===
        // Got: Data from database
        //
        // === CompletableFuture (non-blocking) ===
        // Main thread is free!
        // Got: DATA FROM DATABASE
    }
}

2. Creating CompletableFuture

There are four main ways to create a CompletableFuture, each for a different situation.

2.1 supplyAsync() — Returns a Value

Use supplyAsync() when your async operation produces a result. It takes a Supplier<T> and runs it on a background thread from ForkJoinPool.commonPool().

2.2 runAsync() — No Return Value

Use runAsync() for fire-and-forget operations that do not return a result: logging, sending notifications, writing to a file. It takes a Runnable and returns CompletableFuture<Void>.

2.3 completedFuture() — Already Done

Use completedFuture() to create a CompletableFuture that is already completed with a known value. This is useful for caching, default values, and testing.

2.4 Custom Executor

By default, supplyAsync() and runAsync() use ForkJoinPool.commonPool(). For I/O-bound operations (database calls, HTTP requests, file reads), you should provide a custom executor to avoid starving the common pool.

import java.util.concurrent.*;

public class CreatingCompletableFuture {
    public static void main(String[] args) throws Exception {

        // 1. supplyAsync -- produces a result
        CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("Fetching user on: " + Thread.currentThread().getName());
            return "User{id=1, name='Alice'}";
        });
        System.out.println("User: " + userFuture.join());
        // Output:
        // Fetching user on: ForkJoinPool.commonPool-worker-1
        // User: User{id=1, name='Alice'}

        // 2. runAsync -- no result (fire-and-forget)
        CompletableFuture<Void> logFuture = CompletableFuture.runAsync(() -> {
            System.out.println("Logging on: " + Thread.currentThread().getName());
            // Write to log file, send notification, etc.
        });
        logFuture.join(); // Wait for completion (returns null)

        // 3. completedFuture -- already has a value
        CompletableFuture<String> cached = CompletableFuture.completedFuture("Cached Result");
        System.out.println("Cached: " + cached.join());
        // Output: Cached: Cached Result
        // No thread was created -- the value was immediately available

        // 4. Custom executor -- for I/O operations
        ExecutorService ioPool = Executors.newFixedThreadPool(10, r -> {
            Thread t = new Thread(r);
            t.setDaemon(true);
            t.setName("io-pool-" + t.getId());
            return t;
        });

        CompletableFuture<String> dbResult = CompletableFuture.supplyAsync(() -> {
            System.out.println("DB query on: " + Thread.currentThread().getName());
            try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); }
            return "Query result";
        }, ioPool); // Pass custom executor as second argument

        System.out.println("DB: " + dbResult.join());
        // Output:
        // DB query on: io-pool-25
        // DB: Query result

        // 5. Manually completing
        CompletableFuture<String> manual = new CompletableFuture<>();
        // Some other thread or callback will complete this later
        new Thread(() -> {
            try { Thread.sleep(200); } catch (InterruptedException e) { throw new RuntimeException(e); }
            manual.complete("Manually completed!");
        }).start();
        System.out.println("Manual: " + manual.join());
        // Output: Manual: Manually completed!

        ioPool.shutdown();
    }
}

Creation Methods Summary

Method Returns Input Thread Pool Use Case
supplyAsync(supplier) CompletableFuture<T> Supplier<T> Common pool Async computation with result
supplyAsync(supplier, executor) CompletableFuture<T> Supplier<T> Custom executor I/O-bound operations
runAsync(runnable) CompletableFuture<Void> Runnable Common pool Fire-and-forget side effects
completedFuture(value) CompletableFuture<T> T None (immediate) Caching, defaults, testing
new CompletableFuture() CompletableFuture<T> None None (manual) Bridging callback APIs

3. Transforming Results

Once you have a CompletableFuture, you can transform its result when it arrives using three callback methods. The key difference is what each callback accepts and returns.

Method Input Returns Analogy
thenApply(Function) Previous result Transformed result Stream’s map()
thenAccept(Consumer) Previous result Void Stream’s forEach()
thenRun(Runnable) Nothing Void “Then do this, regardless of result”
import java.util.concurrent.CompletableFuture;

public class TransformingResults {
    public static void main(String[] args) {

        // thenApply -- transform the result (like map)
        System.out.println("=== thenApply ===");
        CompletableFuture<String> greeting = CompletableFuture
            .supplyAsync(() -> "hello")
            .thenApply(s -> s + " world")       // "hello" -> "hello world"
            .thenApply(String::toUpperCase);     // "hello world" -> "HELLO WORLD"

        System.out.println(greeting.join());
        // Output: HELLO WORLD

        // Chain of transformations
        CompletableFuture<Integer> wordCount = CompletableFuture
            .supplyAsync(() -> "The quick brown fox jumps over the lazy dog")
            .thenApply(String::trim)
            .thenApply(s -> s.split("\\s+"))
            .thenApply(words -> words.length);

        System.out.println("Word count: " + wordCount.join());
        // Output: Word count: 9

        // thenAccept -- consume the result (no return value)
        System.out.println("\n=== thenAccept ===");
        CompletableFuture<Void> printFuture = CompletableFuture
            .supplyAsync(() -> "User{name='Alice', age=30}")
            .thenAccept(user -> System.out.println("Received: " + user));
        printFuture.join();
        // Output: Received: User{name='Alice', age=30}

        // thenRun -- run something after completion (ignores result)
        System.out.println("\n=== thenRun ===");
        CompletableFuture<Void> pipeline = CompletableFuture
            .supplyAsync(() -> {
                System.out.println("Step 1: Fetching data...");
                return "raw data";
            })
            .thenApply(data -> {
                System.out.println("Step 2: Processing: " + data);
                return "processed " + data;
            })
            .thenAccept(result -> {
                System.out.println("Step 3: Saving: " + result);
            })
            .thenRun(() -> {
                System.out.println("Step 4: All done! (cleanup)");
            });

        pipeline.join();
        // Output:
        // Step 1: Fetching data...
        // Step 2: Processing: raw data
        // Step 3: Saving: processed raw data
        // Step 4: All done! (cleanup)
    }
}

4. Composing Futures

Composing means chaining async operations where each step depends on the result of the previous one. There are two key methods:

  • thenCompose() — Use when your transformation function itself returns a CompletableFuture. This is the async equivalent of flatMap() in streams. Without it, you end up with CompletableFuture<CompletableFuture<T>> — a future wrapped inside a future.
  • thenCombine() — Use when you want to run two independent futures in parallel and combine their results when both complete.

thenCompose vs thenApply

Scenario Use Why
Transform: T -> U thenApply() Synchronous transformation, returns a plain value
Chain: T -> CompletableFuture<U> thenCompose() Async step that itself returns a future (avoids nesting)
Merge two independent results thenCombine() Both futures run in parallel, combine when both finish
import java.util.concurrent.CompletableFuture;

public class ComposingFutures {

    // Simulated async service methods
    static CompletableFuture<String> fetchUser(int userId) {
        return CompletableFuture.supplyAsync(() -> {
            System.out.println("  Fetching user " + userId + "...");
            try { Thread.sleep(200); } catch (InterruptedException e) { throw new RuntimeException(e); }
            return "User{id=" + userId + ", name='Alice'}";
        });
    }

    static CompletableFuture<String> fetchOrders(String user) {
        return CompletableFuture.supplyAsync(() -> {
            System.out.println("  Fetching orders for " + user + "...");
            try { Thread.sleep(200); } catch (InterruptedException e) { throw new RuntimeException(e); }
            return "Orders[{id=101, item='Laptop'}, {id=102, item='Mouse'}]";
        });
    }

    static CompletableFuture<Double> fetchCreditScore(int userId) {
        return CompletableFuture.supplyAsync(() -> {
            System.out.println("  Fetching credit score for user " + userId + "...");
            try { Thread.sleep(300); } catch (InterruptedException e) { throw new RuntimeException(e); }
            return 750.0;
        });
    }

    public static void main(String[] args) {

        // === thenCompose: Sequential dependency ===
        // Step 1: Fetch user -> Step 2: Fetch orders for that user
        System.out.println("=== thenCompose (sequential chain) ===");

        CompletableFuture<String> orderPipeline = fetchUser(1)
            .thenCompose(user -> fetchOrders(user)); // user result feeds into fetchOrders

        System.out.println("Result: " + orderPipeline.join());
        // Output:
        //   Fetching user 1...
        //   Fetching orders for User{id=1, name='Alice'}...
        // Result: Orders[{id=101, item='Laptop'}, {id=102, item='Mouse'}]

        // WHY NOT thenApply? It would create nested futures:
        // CompletableFuture<CompletableFuture<String>> nested =
        //     fetchUser(1).thenApply(user -> fetchOrders(user)); // WRONG! Nested future

        // === thenCombine: Parallel merge ===
        // Fetch user data AND credit score simultaneously, then combine
        System.out.println("\n=== thenCombine (parallel merge) ===");

        CompletableFuture<String> userFuture = fetchUser(1);
        CompletableFuture<Double> creditFuture = fetchCreditScore(1);

        CompletableFuture<String> combined = userFuture.thenCombine(
            creditFuture,
            (user, creditScore) -> user + " | Credit Score: " + creditScore
        );

        System.out.println("Combined: " + combined.join());
        // Both ran in parallel! Total time is ~300ms, not 200+300=500ms
        // Output: Combined: User{id=1, name='Alice'} | Credit Score: 750.0

        // === Complex chain: compose + combine ===
        System.out.println("\n=== Complex Pipeline ===");

        CompletableFuture<String> fullReport = fetchUser(1)
            .thenCompose(user -> {
                // After getting user, fetch orders AND credit score in parallel
                CompletableFuture<String> orders = fetchOrders(user);
                CompletableFuture<Double> credit = fetchCreditScore(1);

                // Combine both results
                return orders.thenCombine(credit, (o, c) ->
                    "Report: " + user + "\n  " + o + "\n  Credit: " + c);
            });

        System.out.println(fullReport.join());
        // Output:
        // Report: User{id=1, name='Alice'}
        //   Orders[{id=101, item='Laptop'}, {id=102, item='Mouse'}]
        //   Credit: 750.0
    }
}

5. Combining Multiple Futures

When you need to run many async operations in parallel and wait for all (or any) of them to complete, use allOf() and anyOf().

allOf() — Wait for All

CompletableFuture.allOf() takes a varargs of futures and returns a CompletableFuture<Void> that completes when all input futures complete. It does not directly give you the results — you have to extract them from the individual futures.

anyOf() — First to Finish

CompletableFuture.anyOf() returns a CompletableFuture<Object> that completes as soon as any one of the input futures completes. Useful for racing multiple data sources.

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

public class CombiningFutures {
    public static void main(String[] args) {

        // === allOf: Parallel fetch multiple resources ===
        System.out.println("=== allOf: Wait for ALL ===");

        List<String> urls = List.of(
            "https://api.example.com/users",
            "https://api.example.com/orders",
            "https://api.example.com/products",
            "https://api.example.com/reviews"
        );

        // Create a future for each URL
        List<CompletableFuture<String>> futures = urls.stream()
            .map(url -> CompletableFuture.supplyAsync(() -> {
                // Simulate API call with varying delays
                int delay = 100 + new Random().nextInt(400);
                try { Thread.sleep(delay); } catch (InterruptedException e) { throw new RuntimeException(e); }
                return "Response from " + url.substring(url.lastIndexOf('/') + 1)
                    + " (" + delay + "ms)";
            }))
            .collect(Collectors.toList());

        // Wait for ALL to complete
        long start = System.currentTimeMillis();
        CompletableFuture<Void> allDone = CompletableFuture.allOf(
            futures.toArray(new CompletableFuture[0])
        );

        // Collect all results
        CompletableFuture<List<String>> allResults = allDone.thenApply(v ->
            futures.stream()
                .map(CompletableFuture::join) // Safe -- all are already complete
                .collect(Collectors.toList())
        );

        List<String> results = allResults.join();
        long elapsed = System.currentTimeMillis() - start;

        results.forEach(r -> System.out.println("  " + r));
        System.out.println("Total time: " + elapsed + "ms (parallel, not sequential!)");
        // All 4 API calls ran simultaneously
        // Total time is ~max(individual times), not sum

        // === anyOf: Race multiple sources ===
        System.out.println("\n=== anyOf: First to finish ===");

        CompletableFuture<String> primary = CompletableFuture.supplyAsync(() -> {
            try { Thread.sleep(300); } catch (InterruptedException e) { throw new RuntimeException(e); }
            return "Primary DB: user data";
        });

        CompletableFuture<String> replica = CompletableFuture.supplyAsync(() -> {
            try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); }
            return "Replica DB: user data";
        });

        CompletableFuture<String> cache = CompletableFuture.supplyAsync(() -> {
            try { Thread.sleep(50); } catch (InterruptedException e) { throw new RuntimeException(e); }
            return "Cache: user data";
        });

        CompletableFuture<Object> fastest = CompletableFuture.anyOf(primary, replica, cache);
        System.out.println("Fastest response: " + fastest.join());
        // Output: Fastest response: Cache: user data
        // Cache was fastest at 50ms

        // === Practical: Collecting typed results ===
        System.out.println("\n=== Collecting Results with Type Safety ===");

        CompletableFuture<Integer> usersCount = CompletableFuture.supplyAsync(() -> 1500);
        CompletableFuture<Integer> ordersCount = CompletableFuture.supplyAsync(() -> 3200);
        CompletableFuture<Integer> productsCount = CompletableFuture.supplyAsync(() -> 850);

        CompletableFuture<String> dashboard = usersCount
            .thenCombine(ordersCount, (users, orders) -> "Users: " + users + ", Orders: " + orders)
            .thenCombine(productsCount, (partial, products) -> partial + ", Products: " + products);

        System.out.println("Dashboard: " + dashboard.join());
        // Output: Dashboard: Users: 1500, Orders: 3200, Products: 850
    }
}

6. Exception Handling

Exceptions in async pipelines are tricky because they happen on background threads. CompletableFuture provides three methods for handling errors, each with a different purpose:

Method Receives Returns Purpose
exceptionally(Function) Exception only Recovery value Provide a fallback when error occurs
handle(BiFunction) Result AND exception New result Handle both success and failure in one place
whenComplete(BiConsumer) Result AND exception Same result (no transform) Logging, cleanup, side effects

Key rule: If an exception occurs in a CompletableFuture and is not handled, it propagates down the chain. Every subsequent thenApply(), thenAccept(), etc. is skipped until an exception handler is encountered.

import java.util.concurrent.CompletableFuture;

public class ExceptionHandling {
    public static void main(String[] args) {

        // === exceptionally: Provide a fallback ===
        System.out.println("=== exceptionally ===");

        CompletableFuture<String> withFallback = CompletableFuture
            .supplyAsync(() -> {
                if (true) throw new RuntimeException("Database connection failed!");
                return "data";
            })
            .exceptionally(ex -> {
                System.out.println("Error: " + ex.getMessage());
                return "default data"; // Recovery value
            });

        System.out.println("Result: " + withFallback.join());
        // Output:
        // Error: Database connection failed!
        // Result: default data

        // === handle: Both success and failure ===
        System.out.println("\n=== handle (success case) ===");

        CompletableFuture<String> handled = CompletableFuture
            .supplyAsync(() -> "real data")
            .handle((result, ex) -> {
                if (ex != null) {
                    System.out.println("Failed: " + ex.getMessage());
                    return "fallback";
                }
                System.out.println("Success: " + result);
                return result.toUpperCase();
            });
        System.out.println("Result: " + handled.join());
        // Output:
        // Success: real data
        // Result: REAL DATA

        System.out.println("\n=== handle (failure case) ===");

        CompletableFuture<String> handleError = CompletableFuture
            .<String>supplyAsync(() -> {
                throw new RuntimeException("Network timeout");
            })
            .handle((result, ex) -> {
                if (ex != null) {
                    System.out.println("Failed: " + ex.getMessage());
                    return "cached result";
                }
                return result;
            });
        System.out.println("Result: " + handleError.join());
        // Output:
        // Failed: Network timeout
        // Result: cached result

        // === whenComplete: Logging/cleanup (does NOT change result) ===
        System.out.println("\n=== whenComplete ===");

        CompletableFuture<String> logged = CompletableFuture
            .supplyAsync(() -> "important data")
            .whenComplete((result, ex) -> {
                if (ex != null) {
                    System.out.println("LOG: Operation failed - " + ex.getMessage());
                } else {
                    System.out.println("LOG: Operation succeeded - " + result);
                }
                // NOTE: Cannot change the result here!
            });
        System.out.println("Result: " + logged.join());
        // Output:
        // LOG: Operation succeeded - important data
        // Result: important data

        // === Exception propagation ===
        System.out.println("\n=== Exception Propagation ===");

        CompletableFuture<String> chain = CompletableFuture
            .supplyAsync(() -> "data")
            .thenApply(s -> {
                throw new RuntimeException("Step 2 failed");
            })
            .thenApply(s -> {
                System.out.println("This is SKIPPED");
                return "never reached";
            })
            .thenApply(s -> {
                System.out.println("This is ALSO SKIPPED");
                return "never reached";
            })
            .exceptionally(ex -> {
                System.out.println("Caught at the end: " + ex.getMessage());
                return "recovered";
            });

        System.out.println("Result: " + chain.join());
        // Output:
        // Caught at the end: java.lang.RuntimeException: Step 2 failed
        // Result: recovered
    }
}

7. Async Variants

Every callback method in CompletableFuture has three variants:

Variant Thread Used When to Use
thenApply(fn) Same thread that completed the previous stage (or the caller thread if already complete) Fast, CPU-light transformations
thenApplyAsync(fn) ForkJoinPool.commonPool() CPU-intensive transformations
thenApplyAsync(fn, executor) Your custom executor I/O-bound operations (DB, HTTP)

The same pattern applies to thenAccept/thenAcceptAsync, thenRun/thenRunAsync, thenCompose/thenComposeAsync, thenCombine/thenCombineAsync, etc.

Rule of thumb:

  • Use the non-async version for quick transformations (string manipulation, mapping, filtering).
  • Use Async with a custom executor for anything that involves I/O or takes more than a few milliseconds.
  • Never use the common pool (default Async) for blocking I/O — it has limited threads and is shared across your entire application.
import java.util.concurrent.*;

public class AsyncVariants {
    public static void main(String[] args) {
        // Custom executor for I/O work
        ExecutorService ioPool = Executors.newFixedThreadPool(4, r -> {
            Thread t = new Thread(r);
            t.setDaemon(true);
            t.setName("io-" + t.getId());
            return t;
        });

        // thenApply -- runs on completing thread (fast)
        CompletableFuture<String> sync = CompletableFuture
            .supplyAsync(() -> {
                System.out.println("Produce on: " + Thread.currentThread().getName());
                return "data";
            })
            .thenApply(s -> {
                System.out.println("thenApply on: " + Thread.currentThread().getName());
                return s.toUpperCase(); // Quick transformation, same thread
            });
        sync.join();
        // Both likely run on: ForkJoinPool.commonPool-worker-1

        System.out.println();

        // thenApplyAsync -- runs on common pool (different thread)
        CompletableFuture<String> async = CompletableFuture
            .supplyAsync(() -> {
                System.out.println("Produce on: " + Thread.currentThread().getName());
                return "data";
            })
            .thenApplyAsync(s -> {
                System.out.println("thenApplyAsync on: " + Thread.currentThread().getName());
                return s.toUpperCase(); // May run on a different thread
            });
        async.join();

        System.out.println();

        // thenApplyAsync with executor -- runs on YOUR thread pool
        CompletableFuture<String> customAsync = CompletableFuture
            .supplyAsync(() -> {
                System.out.println("Produce on: " + Thread.currentThread().getName());
                return "data";
            })
            .thenApplyAsync(s -> {
                System.out.println("Custom async on: " + Thread.currentThread().getName());
                // Simulate I/O (database call)
                try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); }
                return "DB result for: " + s;
            }, ioPool);

        System.out.println("Result: " + customAsync.join());
        // Output: Custom async on: io-25
        // Result: DB result for: data

        ioPool.shutdown();
    }
}

8. Real-World Patterns

These patterns solve common problems you will encounter in production code.

8.1 Parallel API Calls

When you need data from multiple independent sources, fetch them all in parallel instead of sequentially. This can dramatically reduce response times.

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

public class ParallelApiCalls {

    static ExecutorService httpPool = Executors.newFixedThreadPool(10);

    // Simulated API calls
    static CompletableFuture<Map<String, Object>> fetchUserProfile(int userId) {
        return CompletableFuture.supplyAsync(() -> {
            sleep(200);
            return Map.of("id", userId, "name", "Alice", "email", "alice@example.com");
        }, httpPool);
    }

    static CompletableFuture<List<String>> fetchUserOrders(int userId) {
        return CompletableFuture.supplyAsync(() -> {
            sleep(300);
            return List.of("Order-101", "Order-102", "Order-103");
        }, httpPool);
    }

    static CompletableFuture<Map<String, Object>> fetchUserPreferences(int userId) {
        return CompletableFuture.supplyAsync(() -> {
            sleep(150);
            return Map.of("theme", "dark", "language", "en", "notifications", true);
        }, httpPool);
    }

    public static void main(String[] args) {
        int userId = 1;
        long start = System.currentTimeMillis();

        // Kick off ALL requests simultaneously
        CompletableFuture<Map<String, Object>> profileFuture = fetchUserProfile(userId);
        CompletableFuture<List<String>> ordersFuture = fetchUserOrders(userId);
        CompletableFuture<Map<String, Object>> prefsFuture = fetchUserPreferences(userId);

        // Wait for all and combine
        CompletableFuture<Map<String, Object>> dashboard = CompletableFuture
            .allOf(profileFuture, ordersFuture, prefsFuture)
            .thenApply(v -> {
                Map<String, Object> result = new HashMap<>();
                result.put("profile", profileFuture.join());
                result.put("orders", ordersFuture.join());
                result.put("preferences", prefsFuture.join());
                return result;
            });

        Map<String, Object> result = dashboard.join();
        long elapsed = System.currentTimeMillis() - start;

        System.out.println("Dashboard data:");
        result.forEach((key, value) -> System.out.println("  " + key + ": " + value));
        System.out.println("Total time: " + elapsed + "ms");
        // Total time: ~300ms (max of 200, 300, 150) instead of 650ms (sequential)

        httpPool.shutdown();
    }

    static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) { throw new RuntimeException(e); }
    }
}

8.2 Timeout Handling

In production, you cannot wait forever for an async operation to complete. You need timeouts.

import java.util.concurrent.*;

public class TimeoutHandling {
    public static void main(String[] args) {

        // === Java 9+: orTimeout() and completeOnTimeout() ===

        // orTimeout: Fails with TimeoutException if not complete in time
        CompletableFuture<String> withTimeout = CompletableFuture
            .supplyAsync(() -> {
                try { Thread.sleep(5000); } catch (InterruptedException e) { throw new RuntimeException(e); }
                return "slow result";
            })
            .orTimeout(1, TimeUnit.SECONDS)
            .exceptionally(ex -> {
                System.out.println("Timed out: " + ex.getMessage());
                return "timeout fallback";
            });

        System.out.println("orTimeout result: " + withTimeout.join());
        // Output:
        // Timed out: java.util.concurrent.TimeoutException
        // orTimeout result: timeout fallback

        // completeOnTimeout: Provides a default value on timeout (no exception)
        CompletableFuture<String> withDefault = CompletableFuture
            .supplyAsync(() -> {
                try { Thread.sleep(5000); } catch (InterruptedException e) { throw new RuntimeException(e); }
                return "slow result";
            })
            .completeOnTimeout("default value", 1, TimeUnit.SECONDS);

        System.out.println("completeOnTimeout: " + withDefault.join());
        // Output: completeOnTimeout: default value

        // === Java 8 compatible: Manual timeout ===
        System.out.println("\n=== Java 8 Timeout Pattern ===");
        CompletableFuture<String> java8Timeout = timeoutAfter(
            CompletableFuture.supplyAsync(() -> {
                try { Thread.sleep(5000); } catch (InterruptedException e) { throw new RuntimeException(e); }
                return "slow";
            }),
            1, TimeUnit.SECONDS,
            "java8 fallback"
        );
        System.out.println("Java 8 timeout: " + java8Timeout.join());
        // Output: Java 8 timeout: java8 fallback
    }

    // Java 8 compatible timeout helper
    static <T> CompletableFuture<T> timeoutAfter(
            CompletableFuture<T> future, long timeout, TimeUnit unit, T fallback) {
        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
        CompletableFuture<T> timeoutFuture = new CompletableFuture<>();

        scheduler.schedule(() -> timeoutFuture.complete(fallback), timeout, unit);

        return CompletableFuture.anyOf(future, timeoutFuture)
            .thenApply(result -> {
                scheduler.shutdown();
                @SuppressWarnings("unchecked")
                T typed = (T) result;
                return typed;
            });
    }
}

8.3 Retry Pattern

For transient failures (network blips, temporary unavailability), retrying the operation automatically is a common pattern.

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class RetryPattern {

    static AtomicInteger callCount = new AtomicInteger(0);

    // Simulated flaky service: fails first 2 times, succeeds on 3rd
    static CompletableFuture<String> flakyService() {
        return CompletableFuture.supplyAsync(() -> {
            int attempt = callCount.incrementAndGet();
            System.out.println("  Attempt " + attempt + " on " + Thread.currentThread().getName());
            if (attempt <= 2) {
                throw new RuntimeException("Service unavailable (attempt " + attempt + ")");
            }
            return "Success on attempt " + attempt;
        });
    }

    // Retry helper
    static <T> CompletableFuture<T> retry(
            java.util.function.Supplier<CompletableFuture<T>> supplier,
            int maxRetries,
            long delayMs) {

        CompletableFuture<T> future = supplier.get();

        for (int i = 0; i < maxRetries; i++) {
            final int attempt = i + 1;
            future = future.handle((result, ex) -> {
                if (ex == null) {
                    return CompletableFuture.completedFuture(result);
                }
                System.out.println("  Retry " + attempt + ": " + ex.getMessage());
                try { Thread.sleep(delayMs); } catch (InterruptedException e) { throw new RuntimeException(e); }
                return supplier.get();
            }).thenCompose(f -> f);
        }
        return future;
    }

    public static void main(String[] args) {
        System.out.println("=== Retry Pattern ===");

        CompletableFuture<String> result = retry(
            RetryPattern::flakyService,
            3,        // max retries
            500       // delay between retries in ms
        );

        System.out.println("Final result: " + result.join());

        // Output:
        //   Attempt 1 on ForkJoinPool.commonPool-worker-1
        //   Retry 1: java.lang.RuntimeException: Service unavailable (attempt 1)
        //   Attempt 2 on ForkJoinPool.commonPool-worker-2
        //   Retry 2: java.lang.RuntimeException: Service unavailable (attempt 2)
        //   Attempt 3 on ForkJoinPool.commonPool-worker-1
        // Final result: Success on attempt 3
    }
}

9. Best Practices

# Practice Do Do Not
1 Always handle exceptions Use exceptionally() or handle() on every chain Let exceptions silently disappear
2 Use custom executors for I/O supplyAsync(fn, ioPool) Use common pool for DB/HTTP calls
3 Prefer thenCompose over thenApply thenCompose() when calling another async method thenApply() creating nested futures
4 Use join() over get() future.join() — throws unchecked exception future.get() — forces try/catch for checked exception
5 Add timeouts orTimeout() or completeOnTimeout() Wait forever for a result
6 Name your threads Custom ThreadFactory with meaningful names Debug with “pool-3-thread-7” names
7 Use allOf for parallel batch operations allOf(futures) then collect results Sequential loop calling join() on each
import java.util.concurrent.*;

public class BestPracticesDemo {
    public static void main(String[] args) {
        // GOOD: Custom executor for I/O work
        ExecutorService ioPool = Executors.newFixedThreadPool(20, r -> {
            Thread t = new Thread(r);
            t.setDaemon(true);
            t.setName("io-worker-" + t.getId());
            return t;
        });

        // GOOD: Always handle exceptions
        CompletableFuture<String> safe = CompletableFuture
            .supplyAsync(() -> fetchFromDatabase("user-123"), ioPool)
            .thenApply(data -> process(data))
            .exceptionally(ex -> {
                System.err.println("Pipeline failed: " + ex.getMessage());
                return "fallback";
            });
        System.out.println("Safe result: " + safe.join());

        // GOOD: Use join() instead of get()
        String result = safe.join(); // Throws CompletionException (unchecked)
        // NOT: safe.get(); // Throws ExecutionException (checked) -- forces try/catch

        // GOOD: Use thenCompose for async chains
        CompletableFuture<String> composed = CompletableFuture
            .supplyAsync(() -> "user-123", ioPool)
            .thenCompose(id -> fetchAsync(id))     // Returns CompletableFuture
            .thenCompose(user -> enrichAsync(user)) // Chain of async calls
            .exceptionally(ex -> "error: " + ex.getMessage());
        System.out.println("Composed: " + composed.join());

        // BAD: Sequential blocking -- defeats the purpose!
        // for (CompletableFuture<String> f : futures) {
        //     results.add(f.join()); // BLOCKS on each one sequentially!
        // }

        // GOOD: Parallel with allOf
        // CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
        //     .thenApply(v -> futures.stream()
        //         .map(CompletableFuture::join)
        //         .collect(Collectors.toList()));

        ioPool.shutdown();
    }

    static String fetchFromDatabase(String id) {
        return "data for " + id;
    }

    static String process(String data) {
        return "processed: " + data;
    }

    static CompletableFuture<String> fetchAsync(String id) {
        return CompletableFuture.completedFuture("User{" + id + "}");
    }

    static CompletableFuture<String> enrichAsync(String user) {
        return CompletableFuture.completedFuture("Enriched " + user);
    }
}

10. Complete Practical Example — Parallel Order Processing System

This example demonstrates a realistic e-commerce order processing pipeline that uses CompletableFuture to parallelize inventory checks, payment processing, and notification delivery.

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

public class OrderProcessingSystem {

    // Thread pool for I/O operations
    static final ExecutorService IO_POOL = Executors.newFixedThreadPool(10, r -> {
        Thread t = new Thread(r);
        t.setDaemon(true);
        t.setName("io-worker-" + t.getId());
        return t;
    });

    // --- Domain classes ---
    record Order(String orderId, String customerId, List<OrderItem> items) {}
    record OrderItem(String productId, String name, double price, int quantity) {}
    record InventoryResult(String productId, boolean available, int stock) {}
    record PaymentResult(String transactionId, boolean success, String message) {}
    record ShippingResult(String trackingNumber, String estimatedDelivery) {}

    // --- Service simulations ---

    static CompletableFuture<InventoryResult> checkInventory(OrderItem item) {
        return CompletableFuture.supplyAsync(() -> {
            sleep(100 + new Random().nextInt(200));
            boolean available = new Random().nextInt(10) > 1; // 90% available
            int stock = available ? 10 + new Random().nextInt(90) : 0;
            System.out.printf("  [Inventory] %s: %s (stock: %d)%n",
                item.name(), available ? "AVAILABLE" : "OUT OF STOCK", stock);
            return new InventoryResult(item.productId(), available, stock);
        }, IO_POOL);
    }

    static CompletableFuture<PaymentResult> processPayment(String customerId, double total) {
        return CompletableFuture.supplyAsync(() -> {
            sleep(300);
            String txnId = "TXN-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
            boolean success = new Random().nextInt(10) > 0; // 90% success
            String message = success ? "Payment approved" : "Insufficient funds";
            System.out.printf("  [Payment] Customer %s, $%.2f: %s (%s)%n",
                customerId, total, message, txnId);
            if (!success) {
                throw new RuntimeException("Payment failed: " + message);
            }
            return new PaymentResult(txnId, true, message);
        }, IO_POOL);
    }

    static CompletableFuture<ShippingResult> createShipment(Order order) {
        return CompletableFuture.supplyAsync(() -> {
            sleep(200);
            String tracking = "SHIP-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
            System.out.printf("  [Shipping] Order %s: tracking %s%n", order.orderId(), tracking);
            return new ShippingResult(tracking, "3-5 business days");
        }, IO_POOL);
    }

    static CompletableFuture<Void> sendEmail(String to, String subject) {
        return CompletableFuture.runAsync(() -> {
            sleep(100);
            System.out.printf("  [Email] To: %s | Subject: %s%n", to, subject);
        }, IO_POOL);
    }

    static CompletableFuture<Void> sendSms(String to, String message) {
        return CompletableFuture.runAsync(() -> {
            sleep(50);
            System.out.printf("  [SMS] To: %s | Message: %s%n", to, message);
        }, IO_POOL);
    }

    // --- Main processing pipeline ---

    static CompletableFuture<String> processOrder(Order order) {
        System.out.println("\nProcessing order: " + order.orderId());
        long startTime = System.currentTimeMillis();

        // Step 1: Check inventory for ALL items in PARALLEL
        System.out.println("Step 1: Checking inventory...");
        List<CompletableFuture<InventoryResult>> inventoryChecks = order.items().stream()
            .map(OrderProcessingSystem::checkInventory)
            .collect(Collectors.toList());

        CompletableFuture<List<InventoryResult>> allInventory = CompletableFuture
            .allOf(inventoryChecks.toArray(new CompletableFuture[0]))
            .thenApply(v -> inventoryChecks.stream()
                .map(CompletableFuture::join)
                .collect(Collectors.toList()));

        // Step 2: Verify all items available, then process payment
        return allInventory
            .thenCompose(inventoryResults -> {
                // Check if any item is out of stock
                List<String> outOfStock = inventoryResults.stream()
                    .filter(r -> !r.available())
                    .map(InventoryResult::productId)
                    .collect(Collectors.toList());

                if (!outOfStock.isEmpty()) {
                    CompletableFuture<String> failed = new CompletableFuture<>();
                    failed.completeExceptionally(
                        new RuntimeException("Items out of stock: " + outOfStock));
                    return failed;
                }

                // Calculate total
                double total = order.items().stream()
                    .mapToDouble(item -> item.price() * item.quantity())
                    .sum();

                System.out.println("Step 2: Processing payment ($" + String.format("%.2f", total) + ")...");
                return processPayment(order.customerId(), total);
            })
            // Step 3: Create shipment
            .thenCompose(payment -> {
                System.out.println("Step 3: Creating shipment...");
                return createShipment(order)
                    .thenApply(shipping -> Map.of(
                        "payment", payment,
                        "shipping", shipping
                    ));
            })
            // Step 4: Send notifications in PARALLEL
            .thenCompose(results -> {
                System.out.println("Step 4: Sending notifications...");
                PaymentResult payment = (PaymentResult) results.get("payment");
                ShippingResult shipping = (ShippingResult) results.get("shipping");

                CompletableFuture<Void> email = sendEmail(
                    order.customerId() + "@example.com",
                    "Order " + order.orderId() + " confirmed! Tracking: " + shipping.trackingNumber()
                );
                CompletableFuture<Void> sms = sendSms(
                    order.customerId(),
                    "Order confirmed! Track: " + shipping.trackingNumber()
                );

                return CompletableFuture.allOf(email, sms)
                    .thenApply(v -> String.format(
                        "Order %s COMPLETE | Payment: %s | Tracking: %s | Delivery: %s",
                        order.orderId(), payment.transactionId(),
                        shipping.trackingNumber(), shipping.estimatedDelivery()
                    ));
            })
            // Handle errors
            .exceptionally(ex -> {
                String error = "Order " + order.orderId() + " FAILED: " + ex.getMessage();
                System.out.println("  [ERROR] " + error);
                // Send failure notification
                sendEmail(order.customerId() + "@example.com",
                    "Order " + order.orderId() + " could not be processed").join();
                return error;
            })
            .whenComplete((result, ex) -> {
                long elapsed = System.currentTimeMillis() - startTime;
                System.out.println("  [Timer] Order " + order.orderId() + " took " + elapsed + "ms");
            });
    }

    public static void main(String[] args) {
        System.out.println("====================================");
        System.out.println("  Order Processing System");
        System.out.println("====================================");

        // Create sample orders
        Order order1 = new Order("ORD-001", "CUST-100", List.of(
            new OrderItem("PROD-1", "Laptop", 999.99, 1),
            new OrderItem("PROD-2", "Mouse", 29.99, 2),
            new OrderItem("PROD-3", "Keyboard", 79.99, 1)
        ));

        Order order2 = new Order("ORD-002", "CUST-200", List.of(
            new OrderItem("PROD-4", "Monitor", 399.99, 1),
            new OrderItem("PROD-5", "Headset", 149.99, 1)
        ));

        Order order3 = new Order("ORD-003", "CUST-300", List.of(
            new OrderItem("PROD-6", "Webcam", 89.99, 1)
        ));

        // Process ALL orders in PARALLEL
        List<Order> orders = List.of(order1, order2, order3);
        long totalStart = System.currentTimeMillis();

        List<CompletableFuture<String>> orderFutures = orders.stream()
            .map(OrderProcessingSystem::processOrder)
            .collect(Collectors.toList());

        // Wait for all orders to complete
        CompletableFuture.allOf(orderFutures.toArray(new CompletableFuture[0])).join();

        // Print final results
        System.out.println("\n====================================");
        System.out.println("  FINAL RESULTS");
        System.out.println("====================================");

        for (CompletableFuture<String> future : orderFutures) {
            System.out.println("  " + future.join());
        }

        long totalElapsed = System.currentTimeMillis() - totalStart;
        System.out.println("\nTotal processing time: " + totalElapsed + "ms");
        System.out.println("(All " + orders.size() + " orders processed in parallel)");

        // Summary of concepts used
        System.out.println("\n====================================");
        System.out.println("  Concepts Demonstrated");
        System.out.println("====================================");
        System.out.println("  1.  supplyAsync()     - Async computation with result");
        System.out.println("  2.  runAsync()        - Fire-and-forget async tasks");
        System.out.println("  3.  Custom executor   - Dedicated I/O thread pool");
        System.out.println("  4.  thenApply()       - Transform results");
        System.out.println("  5.  thenCompose()     - Chain dependent async operations");
        System.out.println("  6.  thenCombine()     - (shown in earlier examples)");
        System.out.println("  7.  allOf()           - Wait for all parallel tasks");
        System.out.println("  8.  exceptionally()   - Error recovery with fallback");
        System.out.println("  9.  whenComplete()    - Logging/timing side effects");
        System.out.println("  10. completeExceptionally() - Manual failure");
        System.out.println("  11. completedFuture() - (shown in earlier examples)");
        System.out.println("  12. Parallel streams  - Collecting future results");

        IO_POOL.shutdown();
    }

    static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) { throw new RuntimeException(e); }
    }
}

// Sample Output:
// ====================================
//   Order Processing System
// ====================================
//
// Processing order: ORD-001
// Step 1: Checking inventory...
// Processing order: ORD-002
// Step 1: Checking inventory...
// Processing order: ORD-003
// Step 1: Checking inventory...
//   [Inventory] Webcam: AVAILABLE (stock: 45)
//   [Inventory] Mouse: AVAILABLE (stock: 72)
//   [Inventory] Keyboard: AVAILABLE (stock: 38)
//   [Inventory] Headset: AVAILABLE (stock: 91)
//   [Inventory] Monitor: AVAILABLE (stock: 23)
//   [Inventory] Laptop: AVAILABLE (stock: 56)
// Step 2: Processing payment ($89.99)...
// Step 2: Processing payment ($549.98)...
// Step 2: Processing payment ($1139.96)...
//   [Payment] Customer CUST-300, $89.99: Payment approved (TXN-A1B2C3D4)
// Step 3: Creating shipment...
//   [Payment] Customer CUST-200, $549.98: Payment approved (TXN-E5F6G7H8)
// Step 3: Creating shipment...
//   [Payment] Customer CUST-100, $1139.96: Payment approved (TXN-I9J0K1L2)
// Step 3: Creating shipment...
//   [Shipping] Order ORD-003: tracking SHIP-M3N4O5P6
// Step 4: Sending notifications...
//   [Shipping] Order ORD-002: tracking SHIP-Q7R8S9T0
// Step 4: Sending notifications...
//   [SMS] To: CUST-300 | Message: Order confirmed! Track: SHIP-M3N4O5P6
//   [Email] To: CUST-300@example.com | Subject: Order ORD-003 confirmed!
//   [Shipping] Order ORD-001: tracking SHIP-U1V2W3X4
// Step 4: Sending notifications...
//   [Timer] Order ORD-003 took 652ms
//   [SMS] To: CUST-200 | Message: Order confirmed! Track: SHIP-Q7R8S9T0
//   [Email] To: CUST-200@example.com | Subject: Order ORD-002 confirmed!
//   [Timer] Order ORD-002 took 785ms
//   [SMS] To: CUST-100 | Message: Order confirmed! Track: SHIP-U1V2W3X4
//   [Email] To: CUST-100@example.com | Subject: Order ORD-001 confirmed!
//   [Timer] Order ORD-001 took 892ms
//
// ====================================
//   FINAL RESULTS
// ====================================
//   Order ORD-001 COMPLETE | Payment: TXN-I9J0K1L2 | Tracking: SHIP-U1V2W3X4
//   Order ORD-002 COMPLETE | Payment: TXN-E5F6G7H8 | Tracking: SHIP-Q7R8S9T0
//   Order ORD-003 COMPLETE | Payment: TXN-A1B2C3D4 | Tracking: SHIP-M3N4O5P6
//
// Total processing time: 895ms
// (All 3 orders processed in parallel)

Quick Reference

Category Method Description
Creating supplyAsync(supplier) Run async, return result
runAsync(runnable) Run async, no result
completedFuture(value) Already-complete future
new CompletableFuture() Manually completable
Transforming thenApply(fn) Transform result (like map)
thenAccept(consumer) Consume result (no return)
thenRun(runnable) Run after completion (ignores result)
Composing thenCompose(fn) Chain async operations (flatMap)
thenCombine(other, fn) Merge two parallel results
Combining allOf(futures...) Wait for all to complete
anyOf(futures...) First to complete wins
Errors exceptionally(fn) Provide fallback on error
handle(bifn) Handle both success and error
whenComplete(biconsumer) Inspect result/error (no transform)
Getting Result join() Block and get result (unchecked exception)
get(timeout, unit) Block with timeout (checked exception)
Timeout (9+) orTimeout(time, unit) Fail with TimeoutException
completeOnTimeout(val, time, unit) Use default value on timeout