⏱️ Section 19 · Question #27

Can you manually complete a CompletableFuture with a result

The primary methods are:


🟢 Junior Level

Yes, you can. Unlike the legacy java.util.concurrent.Future, which served merely as a passive handle on an already running task, CompletableFuture implements the Promise design pattern: you can instantiate an empty instance and manually resolve it with a value or an error from any thread at any time.

The primary methods are:

  • complete(T value) — Transitions the CompletableFuture into a normally completed state with the specified value.
  • completeExceptionally(Throwable ex) — Transitions the CompletableFuture into an exceptionally completed state with the given throwable.
// 1. Create an uncompleted promise (no computation attached yet)
CompletableFuture<String> promise = new CompletableFuture<>();

// 2. Resolve it manually from a background thread or asynchronous event listener:
new Thread(() -> {
    try {
        String data = loadDataFromSocket();
        promise.complete(data); // Manually complete with result
    } catch (Exception ex) {
        promise.completeExceptionally(ex); // Manually propagate failure
    }
}).start();

// 3. Downstream code subscribes to or awaits the result:
promise.thenAccept(data -> System.out.println("Received: " + data));

Core Invariant: First Call Wins

The complete(value) method transitions the future exactly once. If the CompletableFuture has already resolved (via a prior complete(), failure, cancellation, or background completion), subsequent invocations of complete() or completeExceptionally() are simply ignored and return false.


🟡 Middle Level

1. Return Value and Thread Safety of complete()

The complete(T value) method returns a boolean:

  • true — If this specific method call transitioned the CompletableFuture from uncompleted to completed.
  • false — If the CompletableFuture had already resolved prior to this invocation.
CompletableFuture<String> cf = new CompletableFuture<>();

boolean first = cf.complete("Success");      // returns true (winner)
boolean second = cf.complete("Another one"); // returns false (ignored)

System.out.println(cf.join()); // Prints "Success"

The state transition is strictly thread-safe and atomic, relying on internal low-level compare-and-swap (CAS) operations. Data races are eliminated: exactly one thread succeeds in transitioning the state, while all concurrent or subsequent attempts cleanly fail.

2. Manual Exceptional Completion: completeExceptionally()

When an asynchronous external operation fails (socket timeout, payload validation failure, broker error):

CompletableFuture<User> userFuture = new CompletableFuture<>();

// Complete exceptionally:
userFuture.completeExceptionally(new UserNotFoundException("User 42 not found"));

// Subsequent attempts to access the result yield CompletionException:
userFuture.exceptionally(ex -> {
    log.error("Failed to load user: {}", ex.getMessage());
    return User.ANONYMOUS;
});

3. Java 9: completeAsync()

Java 9 introduced completeAsync(Supplier<? extends T> supplier, Executor executor), allowing lazy asynchronous resolution of a future on a designated thread pool:

CompletableFuture<String> cf = new CompletableFuture<>();
// Completes cf asynchronously with supplier's result on customExecutor:
cf.completeAsync(() -> heavyCalculation(), customExecutor);

4. High-Risk Methods: obtrudeValue() and obtrudeException()

The API includes obtrudeValue(T value) and obtrudeException(Throwable ex). These methods forcefully overwrite the completed state of a CompletableFuture, even if it had already resolved:

CompletableFuture<String> cf = CompletableFuture.completedFuture("Original");
cf.obtrudeValue("Overwritten");

System.out.println(cf.join()); // "Overwritten"

[!CAUTION] Why obtrudeValue is strongly discouraged in production: It breaks the immutability invariant of completed stages. Downstream stages (thenApply, thenAccept) that already executed against "Original" will not re-execute. Only subsequent calls to join() or newly attached callbacks observe "Overwritten". This causes state divergence and concurrency bugs. Use it strictly for disaster-recovery utilities or testing harnesses.


🔴 Senior Level

Under the Hood: Treiber Stack and postComplete()

What physically happens inside OpenJDK when calling cf.complete(value)?

public boolean complete(T value) {
    boolean triggered = RESULT.compareAndSet(this, null, (value == null) ? NIL : value);
    if (triggered)
        postComplete(); // Unpacks dependent callback stack!
    return triggered;
}
  1. Atomic CAS: The volatile Object result field transitions from null to the result value (or NIL if value == null, or AltResult if an exception occurred).
  2. postComplete(): The thread that successfully commits the CAS takes responsibility for popping and firing all registered dependent completion stages (Completion) from the lock-free Treiber stack.

Critical Trap: In Which Thread Do Dependent Callbacks Execute?

If synchronous stages (such as .thenApply() or .thenAccept()) were attached to the future before cf.complete("data") is called, those stages execute synchronously on the thread that invoked complete()!

// Netty EventLoop thread handling inbound network frame:
void onChannelRead(ByteBuf buf) {
    String message = parse(buf);
    // CRITICAL HAZARD: if a heavy synchronous computation is chained to `future`:
    future.complete(message); 
    // ALL synchronous callbacks execute RIGHT HERE on the Netty I/O worker thread!
    // The network event loop is now frozen by business logic processing!
}

Remedy: Always decouple thread execution by attaching asynchronous callback variants with a dedicated pool: thenApplyAsync(func, businessExecutor).

Encapsulation: Preventing Unauthorized External Completion

If an internal domain service exposes a raw CompletableFuture<Data>, malicious or poorly implemented client code can invoke .complete(...) externally, usurping internal business state:

public CompletableFuture<Data> fetchData() { ... }

// Problematic client usage:
CompletableFuture<Data> f = service.fetchData();
f.complete(new FakeData()); // Client maliciously completes the future prematurely!

Safe API Defense Patterns:

  1. Return CompletionStage:
    public CompletionStage<Data> fetchData() {
        return internalFuture; // CompletionStage lacks complete() methods!
    }
    

    Caveat: Callers can still cast or invoke stage.toCompletableFuture().complete(...).

  2. Return minimalCompletionStage() (Java 9+):
    public CompletionStage<Data> fetchData() {
        return internalFuture.minimalCompletionStage();
    }
    

    Calling .toCompletableFuture() on a MinimalStage throws UnsupportedOperationException, preventing external tampering completely.

  3. Return a Defensive Copy via copy() (Java 9+):
    public CompletableFuture<Data> fetchData() {
        return internalFuture.copy(); // Completing the copy does not affect internalFuture!
    }
    

4 Tricky Questions

1. In which thread do callbacks execute when cf.complete("done") is called manually?

Answer: If dependent stages (thenApply, thenAccept) were registered before complete() was invoked, they execute directly on the thread that calls complete() (within postComplete()). If the future was already complete before the callback was attached, the callback executes immediately on the thread registering it. To ensure execution runs on an isolated thread pool regardless of timing, always register callbacks using asynchronous variants: thenApplyAsync(..., dedicatedExecutor).

2. What happens if two threads concurrently invoke cf.complete("A") and cf.completeExceptionally(new RuntimeException("B"))?

Answer: A lock-free CAS race occurs on the internal result field.

  • Exactly one thread succeeds in changing result from null to its payload ("A" or AltResult(ex)). That winning thread receives true.
  • The losing thread observes result != null, its CAS fails, the method returns false, and its supplied value or exception is discarded with zero side effects. No exception is thrown.

3. If cf.obtrudeValue("new") is invoked after cf.complete("old") has already executed, do earlier attached stages re-run?

Answer: No, previously attached stages never re-run. When complete("old") fired, postComplete() drained and unlinked the internal Treiber stack of completion dependencies. Calling obtrudeValue("new") modifies the result field, but the dependency stack is already empty. Consequently, existing downstream stages retain "old", while direct calls to cf.join() or newly attached stages observe "new", leading to architectural desynchronization.

4. How can you expose an asynchronous return type from a library without allowing callers to manually complete it?

Answer: In Java 9+, the best approach is returning CompletableFuture.minimalCompletionStage():

public CompletionStage<Order> createOrderAsync(...) {
    CompletableFuture<Order> internalPromise = new CompletableFuture<>();
    // initialize background task...
    return internalPromise.minimalCompletionStage();
}

The CompletionStage interface contains no completion methods (complete, obtrudeValue). Additionally, the MinimalStage implementation guarantees that calling .toCompletableFuture() throws UnsupportedOperationException, preventing callers from bypassing the encapsulation. Alternatively, return internalPromise.copy(), ensuring any external completion affects only the client’s copy.


🎯 Interview Cheat Sheet

Summary in 60 Seconds

  • CompletableFuture can be manually completed from any thread using complete(value) or completeExceptionally(ex).
  • It implements the Promise pattern, ideal for bridging asynchronous event loops (Netty, Kafka, NIO) to Java pipelines.
  • First completion wins atomically via CAS; subsequent calls return false and are ignored.
  • Critical Threading Trap: Synchronous callbacks run inside the thread calling complete(). Never invoke complete() on I/O threads if heavy callbacks are attached.
  • Never use obtrudeValue() in production (breaks immutable completion invariants).
  • Encapsulate internal futures by returning minimalCompletionStage() or copy() to prevent client tampering.

Common Interview Traps

  • Assuming complete() re-triggers already finished futures on subsequent calls.
  • Calling complete() inside a high-throughput Netty I/O thread when synchronous downstream stages perform blocking operations.
  • Using obtrudeValue() as a state reset mechanism.