☕ Java

CompletableFuture

CompletableFuture<T>, introduced in Java 8, is a Future implementation that supports manual completion, non-blocking callbacks, and a rich set of composition and transformation operators that enable building complex asynchronous pipelines without blocking threads. It implements both Future<T> and CompletionStage<T>. The CompletionStage interface defines over 40 methods for chaining, combining, transforming, and handling exceptions in asynchronous computations, all of which return new CompletionStages that complete when their input stage completes and their operation finishes. CompletableFuture can be created from an async computation (supplyAsync, runAsync), completed manually by any thread (complete, completeExceptionally), or combined from multiple CompletableFutures (allOf, anyOf). This entry covers the creation methods, the full transformation and chaining API (thenApply, thenAccept, thenRun, thenCompose, thenCombine and their Async variants), exception handling (exceptionally, handle, whenComplete), fan-out and fan-in patterns (allOf, anyOf), the executor model and the default ForkJoinPool.commonPool(), manual completion for testing and bridging, and performance considerations including thread pool selection for different stage types.

Creation, Manual Completion, and the Executor Model

CompletableFuture is created in three ways: factory methods for async computation, constructors for manual completion, and static combinators for joining multiple futures. supplyAsync(Supplier<U> supplier) submits the supplier to a thread pool and returns a CompletableFuture<U> that completes with the supplier's return value when the supplier finishes. supplyAsync(Supplier<U>, Executor) uses the provided executor; supplyAsync(Supplier<U>) without an executor uses ForkJoinPool.commonPool(). runAsync(Runnable) is the void variant — it returns CompletableFuture<Void> and is appropriate for tasks that produce no result. These two methods are the primary entry points for starting asynchronous computation. The default executor — ForkJoinPool.commonPool() — is the JVM-wide shared pool. Its parallelism is set to availableProcessors - 1 (minimum 1). Using the common pool for I/O-bound work is problematic: blocking tasks starve CPU-bound tasks that also use the common pool (including parallel streams and other CompletableFuture chains). For any I/O-bound supplyAsync, always provide a dedicated I/O executor. For CPU-bound operations, the common pool is appropriate. This is the most common and most impactful configuration mistake in CompletableFuture-based code. new CompletableFuture<T>() creates an incomplete future that no computation will ever automatically complete — it must be completed manually by calling complete(T value), completeExceptionally(Throwable ex), or cancel(boolean mayInterruptIfRunning). Manual completion is useful for bridging callback-based APIs to CompletableFuture: create an incomplete future, pass its complete/completeExceptionally calls as callbacks to the external API, and return the CompletableFuture to the caller. It is also essential for testing — a manually completed CompletableFuture lets you control exactly when and how a future completes in unit tests. CompletableFuture.completedFuture(T value) creates a future that is already completed with a known value. This is useful for synchronous fast paths in code that returns CompletableFuture: if the result is already available (from a cache hit, for example), return completedFuture(value) rather than running an async operation. CompletableFuture.failedFuture(Throwable ex) (Java 9+) creates an already-failed future. These pre-completed futures are propagated synchronously through non-Async stage operators, avoiding thread pool overhead for the already-complete case.
Java
// ── supplyAsync — start an async computation ─────────────────────────
ExecutorService ioPool = Executors.newFixedThreadPool(20);

// Default executor (ForkJoinPool.commonPool) — only for CPU-bound work:
CompletableFuture<Integer> cpuFuture = CompletableFuture.supplyAsync(() -> {
    return IntStream.rangeClosed(1, 1_000_000).sum();  // CPU-bound: uses commonPool OK
});

// Custom executor — always for I/O-bound work:
CompletableFuture<String> ioFuture = CompletableFuture.supplyAsync(
    () -> fetchFromDatabase(42),     // I/O-bound: use dedicated pool
    ioPool
);

// runAsync — no result:
CompletableFuture<Void> fireAndForget = CompletableFuture.runAsync(
    () -> sendEmail("user@example.com"),
    ioPool
);

// ── Manual completion — bridging to callback APIs ─────────────────────
// Example: wrap a callback-based HTTP client into CompletableFuture:
CompletableFuture<String> bridged = new CompletableFuture<>();

httpClient.getAsync("https://api.example.com/data", new Callback() {
    @Override void onSuccess(String body) {
        bridged.complete(body);              // completes the future with the result
    }
    @Override void onFailure(Throwable ex) {
        bridged.completeExceptionally(ex);   // completes the future with an exception
    }
});
// bridged is now a CompletableFuture that completes when the callback fires

// ── Manual completion for testing ────────────────────────────────────
class UserService {
    CompletableFuture<User> fetchUser(long id) {
        return CompletableFuture.supplyAsync(() -> db.findUser(id), ioPool);
    }
}

// In tests: complete manually to control behavior without I/O:
CompletableFuture<User> mockFuture = new CompletableFuture<>();
UserService service = mock(UserService.class);
when(service.fetchUser(42L)).thenReturn(mockFuture);

// Test immediate completion:
mockFuture.complete(new User(42L, "Alice"));

// Test failure:
CompletableFuture<User> failFuture = new CompletableFuture<>();
failFuture.completeExceptionally(new DatabaseException("timeout"));

// ── completedFuture — synchronous fast path ───────────────────────────
Map<Long, User> cache = new ConcurrentHashMap<>();

CompletableFuture<User> fetchWithCache(long id) {
    User cached = cache.get(id);
    if (cached != null) {
        return CompletableFuture.completedFuture(cached);   // no async needed
    }
    return CompletableFuture.supplyAsync(() -> {
        User user = db.findUser(id);
        cache.put(id, user);
        return user;
    }, ioPool);
}

// ── The default executor problem ──────────────────────────────────────
// WRONG: I/O in commonPool starves CPU tasks (parallel streams, etc.):
CompletableFuture<String> badIo = CompletableFuture.supplyAsync(() -> {
    return slowDatabaseCall();   // blocks a commonPool thread — BAD
});

// CORRECT: dedicated I/O pool for I/O operations:
ExecutorService dbPool = Executors.newFixedThreadPool(
    Runtime.getRuntime().availableProcessors() * 10, // I/O-bound: 10x cores
    new NamedThreadFactory("db-pool", false)
);
CompletableFuture<String> goodIo = CompletableFuture.supplyAsync(
    () -> slowDatabaseCall(),
    dbPool    // I/O-bound work on I/O pool — commonPool remains available for CPU work
);

Transformation, Chaining, and Combining — thenApply, thenCompose, thenCombine

CompletableFuture's transformation and chaining operators are the core of its composability. Each operator creates a new CompletableFuture that depends on the current one and adds a transformation, side effect, or combination. Understanding the three main families and their Async variants is essential for writing correct, efficient asynchronous pipelines. thenApply(Function<T, U> fn) transforms the result of the current stage: when the current stage completes with value T, fn is applied to T and the new stage completes with the result U. It is the async equivalent of Stream.map(). By default (the non-Async variant), fn runs in the thread that completes the current stage — which may be the thread pool thread that ran the async computation, or the calling thread if the future is already completed when thenApply is called. thenApplyAsync(fn) explicitly runs fn in the default executor; thenApplyAsync(fn, executor) runs it in the provided executor. Using the Async variant for computationally significant transformations prevents long transformations from running in unexpected thread pools. thenAccept(Consumer<T> action) consumes the result without producing a new value, returning CompletableFuture<Void>. thenRun(Runnable action) runs an action after completion without receiving the result, also returning CompletableFuture<Void>. Both have Async variants. thenCompose(Function<T, CompletionStage<U>> fn) is the critical operator for chaining dependent async operations. When the current stage completes with T, fn is called with T and must return a new CompletableFuture<U>. The composed future completes when this inner future completes. thenCompose "flattens" the nesting: CompletableFuture<CompletableFuture<U>> becomes CompletableFuture<U>. This is the async equivalent of flatMap() or bind. Without thenCompose, chaining two async operations with thenApply produces a CompletableFuture<CompletableFuture<U>>, which cannot be used directly — thenCompose is mandatory for sequential async chains. thenCombine(CompletionStage<U> other, BiFunction<T, U, V> fn) combines two independent futures: when both the current stage and other complete, fn is called with both results and the combined stage completes with V. This is the join operator for independent parallel computations that both must complete before processing can continue. thenAcceptBoth is the void variant (consumer, no result). runAfterBoth runs a Runnable after both complete without receiving either result. allOf(CompletableFuture<?>... cfs) returns a CompletableFuture<Void> that completes when all the provided futures complete. The result of allOf is Void — it provides no mechanism to retrieve the individual results. The standard pattern is to collect the individual futures in a list, call allOf() on them, and then call join() on each individual future in thenApply after allOf completes. anyOf(CompletableFuture<?>... cfs) returns a CompletableFuture<Object> that completes with the result of the first future to complete.
Java
// ── thenApply — transform result ─────────────────────────────────────
CompletableFuture<String> rawData = CompletableFuture.supplyAsync(
    () -> fetchJson(), ioPool
);

CompletableFuture<User> parsedUser = rawData
    .thenApply(json -> parseJson(json, User.class));   // transform String → User

CompletableFuture<String> userEmail = parsedUser
    .thenApply(User::getEmail);    // transform User → String

// Chain directly:
CompletableFuture<String> pipeline = CompletableFuture
    .supplyAsync(() -> fetchJson(), ioPool)
    .thenApply(json -> parseJson(json, User.class))   // in pool thread (fast — OK)
    .thenApplyAsync(user -> enrichFromLdap(user), ioPool)  // needs I/O — use pool
    .thenApply(User::getEmail);    // fast transform — no pool needed

// ── thenCompose — chain dependent async operations ────────────────────
// WRONG: thenApply returns CompletableFuture<CompletableFuture<User>> — nested!
CompletableFuture<CompletableFuture<User>> nested = CompletableFuture
    .supplyAsync(() -> 42L, ioPool)
    .thenApply(id -> fetchUserAsync(id));    // returns a future — nested!

// CORRECT: thenCompose flattens to CompletableFuture<User>:
CompletableFuture<User> user = CompletableFuture
    .supplyAsync(() -> 42L, ioPool)
    .thenCompose(id -> fetchUserAsync(id));   // id → CompletableFuture<User> → flat

// Multi-step sequential async pipeline:
CompletableFuture<OrderConfirmation> orderPipeline = CompletableFuture
    .supplyAsync(() -> validateOrder(order), ioPool)
    .thenCompose(valid  -> reserveInventory(valid), ioPool)
    .thenCompose(reserved -> chargePayment(reserved), ioPool)
    .thenCompose(charged  -> sendConfirmation(charged), ioPool);
// Each step starts only after the previous async step fully completes

// ── thenCombine — combine two independent futures ─────────────────────
CompletableFuture<User>   userFuture  = fetchUserAsync(userId, ioPool);
CompletableFuture<Account> acctFuture = fetchAccountAsync(accountId, ioPool);
// Both fetches run concurrently — neither depends on the other

CompletableFuture<Dashboard> dashboard = userFuture.thenCombine(
    acctFuture,
    (user, account) -> new Dashboard(user, account)  // called when BOTH complete
);

// ── allOf — wait for ALL, then collect results ────────────────────────
List<Long> userIds = List.of(1L, 2L, 3L, 4L, 5L);

// Start all fetches concurrently:
List<CompletableFuture<User>> userFutures = userIds.stream()
    .map(id -> CompletableFuture.supplyAsync(() -> fetchUser(id), ioPool))
    .collect(Collectors.toList());

// Wait for all, then collect results (standard allOf pattern):
CompletableFuture<List<User>> allUsers = CompletableFuture
    .allOf(userFutures.toArray(new CompletableFuture[0]))
    .thenApply(v -> userFutures.stream()
        .map(CompletableFuture::join)   // join() never blocks here — all are done
        .collect(Collectors.toList())
    );

List<User> users = allUsers.get(10, TimeUnit.SECONDS);
System.out.println("Fetched " + users.size() + " users");

// ── anyOf — first to complete wins ───────────────────────────────────
List<String> endpoints = List.of("primary.api.com", "secondary.api.com", "tertiary.api.com");

CompletableFuture<Object> firstResponse = CompletableFuture.anyOf(
    endpoints.stream()
        .map(endpoint -> CompletableFuture.supplyAsync(() -> callEndpoint(endpoint), ioPool))
        .toArray(CompletableFuture[]::new)
);

String response = (String) firstResponse.get();   // first successful response
System.out.println("Got: " + response);

// ── thenAccept and thenRun — side effects ────────────────────────────
CompletableFuture.supplyAsync(() -> processOrder(), ioPool)
    .thenAccept(order -> auditLog.record(order))  // consume result, return Void
    .thenRun(() -> metrics.increment("orders.processed"));  // no result access

Exception Handling, whenComplete, handle, and Practical Patterns

CompletableFuture provides three exception-handling operators that differ in what they receive and what they return. exceptionally(Function<Throwable, T> fn) is the recovery operator: if the current stage completes exceptionally, fn is called with the exception and its return value becomes the result of the new stage (recovering to a normal completion). If the current stage completes normally, exceptionally passes through the value unchanged — fn is not called. This is the async equivalent of a catch block with a recovery value. Like thenApply, exceptionally runs fn in the completing thread and has an exceptionallyAsync variant (Java 12+) for running recovery on a specific executor. handle(BiFunction<T, Throwable, U> fn) receives both the result (or null on exception) and the exception (or null on success) and must produce a new result U. It is always called — on both success and failure — making it the async equivalent of a finally block with result transformation. handle can transform success values, recover from exceptions, or do both. It is more general than exceptionally (which only handles the failure case) and more correct than whenComplete (which cannot transform the result). whenComplete(BiConsumer<T, Throwable> action) is a side-effect observer: it receives the result (or null) and exception (or null) and runs a consumer for logging or cleanup, then passes the original result or exception through unchanged to downstream stages. It does not transform the result or recover from exceptions; it is purely observational. whenComplete is the right tool for adding logging or metrics to a pipeline without altering its values or exception behavior. The ordering of exception handling matters. Exception handling operators only handle exceptions from their direct upstream stage, not from stages further up the chain that have already been handled. exceptionally on a chain of thenApply calls only catches exceptions from all preceding stages. If thenApply throws, the exception propagates down the chain, skipping all non-exception-handling stages, until it reaches an exceptionally, handle, or whenComplete that handles it. A common practical pattern combines thenCompose for sequential async steps, thenCombine for parallel independent steps, exceptionally for error recovery, and whenComplete for cleanup. The key to readable CompletableFuture pipelines is choosing the right operator for each role and not mixing transformation (thenApply) with side effects (thenAccept) or error handling (exceptionally) in the same stage.
Java
// ── exceptionally — recover from failure ─────────────────────────────
CompletableFuture<String> withRecovery = CompletableFuture
    .supplyAsync(() -> {
        if (Math.random() < 0.5) throw new RuntimeException("Network error");
        return "live data";
    }, ioPool)
    .exceptionally(ex -> {
        System.err.println("Recovering from: " + ex.getMessage());
        return "cached fallback";   // recovery value — stage completes normally
    });

System.out.println("Result: " + withRecovery.get());  // "live data" or "cached fallback"

// ── handle — always runs, transforms both success and failure ─────────
CompletableFuture<Response> handled = CompletableFuture
    .supplyAsync(() -> riskyDatabaseCall(), ioPool)
    .handle((data, ex) -> {
        if (ex != null) {
            log.error("DB call failed", ex);
            return Response.error(ex.getMessage());    // transform exception → Response
        }
        return Response.success(data);                 // transform success → Response
    });

// ── whenComplete — observe without altering ───────────────────────────
CompletableFuture<Order> withLogging = CompletableFuture
    .supplyAsync(() -> processOrder(), ioPool)
    .whenComplete((order, ex) -> {
        if (ex != null) {
            log.error("Order processing failed", ex);  // observe failure
            metrics.increment("orders.failed");
        } else {
            log.info("Order processed: " + order.getId());  // observe success
            metrics.increment("orders.succeeded");
        }
        // Does NOT change the result — exception or value passes through unchanged
    });

// ── Exception propagation through chains ──────────────────────────────
CompletableFuture<String> chain = CompletableFuture
    .supplyAsync(() -> "raw data", ioPool)
    .thenApply(s -> { throw new RuntimeException("Step 2 failed"); })  // throws
    .thenApply(s -> s.toUpperCase())   // SKIPPED — exception propagates
    .thenApply(s -> s.trim())          // SKIPPED — exception propagates
    .exceptionally(ex -> "recovered"); // catches the exception from step 2

System.out.println(chain.get());  // "recovered"

// ── Complete practical pipeline ────────────────────────────────────────
record UserProfile(User user, List<Order> orders, AccountBalance balance) {}

CompletableFuture<UserProfile> buildProfile(long userId) {
    // Step 1: Fetch user (sequential first step)
    CompletableFuture<User> userFuture = CompletableFuture
        .supplyAsync(() -> userService.findById(userId), ioPool)
        .exceptionally(ex -> User.anonymous());   // fallback on user fetch failure

    // Step 2 & 3: Fetch orders and balance in PARALLEL after user:
    return userFuture.thenCompose(user -> {
        CompletableFuture<List<Order>> ordersFuture = CompletableFuture
            .supplyAsync(() -> orderService.findByUser(user.getId()), ioPool)
            .exceptionally(ex -> Collections.emptyList());

        CompletableFuture<AccountBalance> balanceFuture = CompletableFuture
            .supplyAsync(() -> accountService.getBalance(user.getId()), ioPool)
            .exceptionally(ex -> AccountBalance.zero());

        // Combine both parallel results:
        return ordersFuture.thenCombine(balanceFuture,
            (orders, balance) -> new UserProfile(user, orders, balance)
        );
    })
    .whenComplete((profile, ex) -> {
        if (ex != null) log.error("Profile build failed for user " + userId, ex);
        else metrics.recordProfileBuildTime(userId);
    });
}

// Usage: non-blocking
buildProfile(42L)
    .thenAccept(profile -> renderPage(profile))
    .exceptionally(ex -> { renderErrorPage(ex); return null; });

// ── Timeout (Java 9+) — complete exceptionally after deadline ─────────
CompletableFuture<String> withTimeout = CompletableFuture
    .supplyAsync(() -> slowExternalCall(), ioPool)
    .orTimeout(2, TimeUnit.SECONDS)    // completes exceptionally with TimeoutException after 2s
    .exceptionally(ex -> ex instanceof TimeoutException ? "timeout-default" : "other-error");

// completeOnTimeout (Java 9+) — complete with value instead of exception:
CompletableFuture<String> withDefault = CompletableFuture
    .supplyAsync(() -> slowExternalCall(), ioPool)
    .completeOnTimeout("default-value", 2, TimeUnit.SECONDS);  // no exception, just a value

Related Topics in Multithreading

Process vs Thread
A process is an independent program in execution with its own isolated memory space, file handles, and system resources, managed by the operating system and separated from all other processes by strict boundaries. A thread is a unit of execution that lives inside a process, sharing that process's memory, heap, and resources with every other thread in the same process. Java programs run inside a JVM process; the JVM itself creates and manages threads, and every Java application starts with at least one thread — the main thread — with additional threads created by the JVM for garbage collection, JIT compilation, signal handling, and other runtime tasks. Understanding the distinction between processes and threads is the foundation for all concurrent programming in Java: it determines what is shared and what is isolated, what is fast and what is expensive, what fails independently and what fails together. This entry covers the OS-level and JVM-level model of processes and threads, the memory model that follows from the shared-versus-isolated distinction, the cost model for creation and context switching, failure isolation and its consequences, inter-process and inter-thread communication mechanisms, and the practical decision of when to use multiple processes versus multiple threads.
Thread Basics
A Java thread is an instance of java.lang.Thread that represents an independent path of execution within the JVM process. Every thread has a lifecycle — from creation through runnable, running, blocked, waiting, timed-waiting, and terminated states — and a set of properties including its name, priority, daemon status, thread group, and uncaught exception handler. The Java memory model specifies what visibility guarantees exist between threads and when writes by one thread are guaranteed to be visible to another. Thread scheduling is controlled by the OS scheduler subject to hints from the JVM via thread priority; the JVM does not provide real-time scheduling guarantees. This entry covers the complete thread lifecycle and its state machine, thread properties and how they affect scheduling and JVM shutdown, the happens-before relationship and why it matters for visibility, daemon threads and their relationship to JVM shutdown, thread interruption as a cooperative cancellation mechanism, and the methods on Thread that every Java developer must understand.
Creating Threads
Java provides three primary abstractions for defining the work a thread will execute: the Thread class itself (subclassed to override run()), the Runnable interface (a task with no return value and no checked exception), and the Callable interface (a task with a return value and a declared checked exception). Each represents a different contract between the task and the infrastructure that runs it. Thread subclassing couples the task definition to the execution mechanism and is the oldest and least flexible approach. Runnable decouples the task from the thread, allowing the same Runnable to be submitted to thread pools, scheduled executors, or wrapped in Thread objects. Callable extends that decoupling to include a return value and exception propagation, returning a Future that allows the caller to retrieve the result or handle exceptions asynchronously. Understanding all three — their contracts, their limitations, and when to use each — is the foundation of concurrent programming in Java before reaching for higher-level constructs.
Thread Lifecycle
The Java thread lifecycle is the complete sequence of states a thread passes through from the moment a Thread object is constructed to the moment its execution ends. Java defines six states in the Thread.State enum — NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED — and the JVM transitions threads between these states in response to specific method calls, lock acquisitions, monitor notifications, timeouts, and exceptions. Each state has a precise meaning, a defined set of entry conditions, and a defined set of exit conditions. Understanding the lifecycle in full is prerequisite knowledge for diagnosing deadlocks, thread leaks, performance bottlenecks in thread dumps, and incorrect synchronization — all of which manifest as threads stuck in specific states. This entry covers every state in the lifecycle with its entry and exit conditions, all legal and illegal state transitions, how thread dumps represent each state, the interaction between lifecycle states and interruption, the effect of uncaught exceptions on lifecycle, and how to observe lifecycle transitions programmatically.