⏱️ Section 19 · Question #3

How to Create a CompletableFuture That is Already Completed with Result

In asynchronous programming, situations frequently arise where the result of a computation is already known in advance. Examples include reading from an in-memory cache, providi...


🟢 Junior Level

In asynchronous programming, situations frequently arise where the result of a computation is already known in advance. Examples include reading from an in-memory cache, providing a static default fallback, or returning predetermined data in unit tests. Rather than breaking the signature of a method returning CompletableFuture<T>, specialized factory methods provide immediate instances without spinning up background threads.

Primary Factory Methods

  1. CompletableFuture.completedFuture(T value) (Java 8+): Returns a new CompletableFuture that is already completed with the given value.
    CompletableFuture<String> cachedFuture = CompletableFuture.completedFuture("Data from cache");
    
    // The result is immediately available without waiting or thread pool allocation:
    String result = cachedFuture.join(); // Returns instantly
    
  2. CompletableFuture.failedFuture(Throwable ex) (Java 9+): Returns an instance that is already completed exceptionally with the provided error.
    CompletableFuture<User> errorFuture = CompletableFuture.failedFuture(
        new UserNotFoundException("User not found")
    );
    
  3. completedStage(T value) and failedStage(Throwable ex) (Java 12+): Returns a read-only CompletionStage<T> interface, protecting the stage against external manual mutation or forced overriding by clients.

🟡 Middle Level

The Conditional Asynchrony Pattern

The most common real-world production pattern for completedFuture is transparent caching. When a cache hit occurs, no background thread or I/O is scheduled:

public CompletableFuture<ProductDetails> getProductDetails(String productId) {
    // 1. Check fast local in-memory cache (e.g. Caffeine)
    ProductDetails cached = localCache.getIfPresent(productId);
    if (cached != null) {
        // Instant synchronous return: 0 thread context-switching overhead
        return CompletableFuture.completedFuture(cached);
    }

    // 2. Cache miss: offload slow remote I/O to a dedicated thread pool
    return CompletableFuture.supplyAsync(() -> remoteCatalogService.fetch(productId), ioExecutor)
        .thenApply(product -> {
            localCache.put(productId, product);
            return product;
        });
}

The Critical Trap: Callback Execution Context

When invoking .thenApply(...) or .thenAccept(...) on an in-flight future, the callback executes on whichever worker thread completes that future.

However, when invoking .thenApply(...) on an already completed future (like one returned by completedFuture):

  • The callback executes SYNCHRONOUSLY on the calling thread within the thenApply method call itself.
// DANGEROUS: If heavyCpuCalculation() is slow, it blocks the calling thread (e.g. Tomcat request or UI thread)!
CompletableFuture.completedFuture(userData)
    .thenApply(user -> heavyCpuCalculation(user)); 

// SAFE: When thread isolation is required, always use the *Async variant with an explicit executor
CompletableFuture.completedFuture(userData)
    .thenApplyAsync(user -> heavyCpuCalculation(user), computeExecutor);

Java 8 Idiom for Failed Pre-Completed Futures

Java 8 lacked failedFuture(). To create an exceptionally pre-completed future in Java 8, use explicit instantiation:

// Java 8 pattern for failed future:
CompletableFuture<String> failed = new CompletableFuture<>();
failed.completeExceptionally(new IllegalStateException("Validation error"));

🔴 Senior Level

Internal Memory State and Allocation Overhead

When calling CompletableFuture.completedFuture(value):

  1. A new CompletableFuture object is allocated on the heap.
  2. The internal result field is assigned (value == null) ? NIL : value (where NIL is an internal sentinel instance of AltResult).
  3. The stack field (Treiber stack for dependent completion stages) remains null.
  4. No threads, work queues, or ForkJoinPool workers are engaged. Method latency is roughly 2–5 nanoseconds.

Highload Optimization: Eliminating Heap Churn for Void

In high-throughput microservices processing hundreds of thousands of operations per second, methods often return CompletableFuture<Void>. Constantly allocating CompletableFuture.completedFuture(null) creates unnecessary allocation churn in the JVM Young Generation.

Zero-Allocation Pattern (as used in Netty and Project Reactor):

public final class AsyncUtils {
    // Single shared completed future singleton across the application:
    public static final CompletableFuture<Void> COMPLETED_VOID = 
        CompletableFuture.completedFuture(null);
        
    private AsyncUtils() {}
}

// Usage in highload code paths:
public CompletableFuture<Void> logEventAsync(Event event) {
    if (!event.isImportant()) {
        return AsyncUtils.COMPLETED_VOID; // Zero heap allocations!
    }
    return CompletableFuture.runAsync(() -> kafkaProducer.send(event), ioExecutor);
}

Architectural Encapsulation: completedStage (Java 12)

The standard CompletableFuture class allows callers to invoke destructive mutations like cf.complete(val) or cf.obtrudeValue(val). Returning raw CompletableFuture from public service interfaces leaks control to external callers.

Java 12 introduced CompletableFuture.completedStage(value):

public CompletionStage<Session> authenticate(Credentials creds) {
    if (creds.isAnonymous()) {
        // Returns MinimalStage: a private JDK implementation where complete()
        // and obtrude() do not exist or throw UnsupportedOperationException
        return CompletableFuture.completedStage(Session.ANONYMOUS);
    }
    return fetchSessionAsync(creds);
}

4 Tricky Questions

1. What happens if you pass null to CompletableFuture.completedFuture(null)? Does it throw NullPointerException?

Answer: No, NullPointerException is not thrown. Inside CompletableFuture, null is safely represented by the static sentinel NIL (an instance of AltResult with a null Throwable). When get() or join() is subsequently called, the implementation unboxes NIL back to null. This is the standard mechanism to create a completed CompletableFuture<Void>.

2. If a method returns CompletableFuture.completedFuture(x) and the caller chains 10 consecutive .thenApply(...) calls without the Async suffix, which thread executes them, and what risk does this introduce in reactive servers (e.g., Netty or Vert.x)?

Answer: All 10 callbacks execute sequentially and synchronously on the exact thread that called the method and constructed the chain. If this execution occurs within a non-blocking network I/O thread (such as a Netty EventLoop), and any step in the pipeline performs computationally intensive logic (e.g., JSON serialization, cryptography, or XML parsing), the entire EventLoop thread will be stalled. This causes severe latency spikes for thousands of other client connections multiplexed on that same event loop.

3. How does CompletableFuture.completedStage(T) differ from a simple cast (CompletionStage<T>) CompletableFuture.completedFuture(T)?

Answer: With a simple interface cast, callers can downcast back to CompletableFuture via (CompletableFuture<T>) stage and invoke mutating methods like complete() or obtrudeValue(). In contrast, completedStage() returns an instance of the private JDK class CompletableFuture.MinimalStage. Attempting to downcast to CompletableFuture produces a ClassCastException. Furthermore, calling stage.toCompletableFuture() creates an independent defensive copy whose mutation will not affect the original stage.

4. What happens when .thenApply(fn) is chained to CompletableFuture.failedFuture(new MyException())? Is fn invoked?

Answer: The function fn is never invoked. A future in an exceptional state immediately propagates that failure downstream. All intermediate stages (thenApply, thenAccept, thenCompose) are bypassed until an exception-handling stage (exceptionally, handle, or whenComplete) is encountered in the pipeline.


🎯 Interview Cheat Sheet

Summary in 60 Seconds

  • Use completedFuture(T) for immediate success, failedFuture(Throwable) for immediate failure (Java 9+), and completedStage(T) for read-only encapsulation (Java 12+).
  • Primary application: conditional asynchrony (instant return on cache hit, async I/O on cache miss) and deterministic unit testing stubs.
  • Critical Caveat: Callbacks attached via non-async methods (thenApply, thenAccept) to an already completed future run synchronously on the caller thread. Use *Async variants when offloading compute-intensive work.
  • In ultra-high-throughput systems, reuse a static CompletableFuture<Void> COMPLETED_VOID to eliminate GC allocation churn.

Common Interview Mistakes

  • Assuming completedFuture submits tasks to the ForkJoinPool (it executes immediately inline in 2-5 ns).
  • Forgetting that Java 9 introduced failedFuture(), manually writing verbose new CompletableFuture() and completeExceptionally() boilerplate.
  • Forgetting that synchronous callbacks on completed futures can inadvertently block sensitive threads (like Netty EventLoops or UI dispatchers).