Question 1 · Section 19

What is CompletableFuture and how does it differ from Future

The main difference from a regular Future:

Language versions: English Russian Ukrainian

🟢 Junior Level

CompletableFuture is a class in Java (introduced in Java 8) that represents the result of an asynchronous operation that is not yet ready but will be in the future.

The main difference from a regular Future:

  • Future — only waiting for the result (get() blocks the thread)
  • CompletableFuture — you can build chains of actions, combine them, handle errors
// Future — need to wait and block the thread
Future<String> future = executor.submit(() -> "Hello");
String result = future.get();  // blocks the thread!

// CompletableFuture — can build a chain
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> "Hello");
cf.thenApply(s -> s + " World")
  .thenAccept(System.out::println);  // non-blocking!

Simple analogy:

  • Future — like a receipt at a restaurant: you wait for the food to be ready
  • CompletableFuture — like delivery: you ordered, and they will bring it to you while you do your own things

🟡 Middle Level

How it works

Future limitations:

// ❌ Future — cannot build a chain
Future<Integer> future = executor.submit(() -> 42);

// Need to block and handle manually
try {
    Integer result = future.get();  // blocks
    Integer doubled = result * 2;
} catch (InterruptedException | ExecutionException e) {
    // manual handling
}

CompletableFuture capabilities:

// ✅ Chain of actions
CompletableFuture<Integer> cf = CompletableFuture.supplyAsync(() -> 42);

cf.thenApply(result -> result * 2)       // transformation
  .thenAccept(doubled -> System.out.println(doubled))  // consumption
  .exceptionally(ex -> {                 // error handling
      System.err.println("Error: " + ex);
      return null;
  });

// Does not block the main thread!

Creation:

// supplyAsync — with return value
CompletableFuture<String> cf1 =
    CompletableFuture.supplyAsync(() -> "Hello");

// runAsync — no return value
CompletableFuture<Void> cf2 =
    CompletableFuture.runAsync(() -> System.out.println("Done"));

Typical mistakes

  1. Forgot Async — executes in the same thread: ```java CompletableFuture.supplyAsync(() -> “Hello”) .thenApply(s -> { // Executes in ForkJoinPool.commonPool() return s + “ World”; }); // This is NOT a mistake, it’s a deliberate choice. thenApply in the same thread — // the right choice for lightweight CPU transformations. The problem only arises if // thenApply performs a blocking operation.

// vs CompletableFuture cf = CompletableFuture.completedFuture("Hello"); cf.thenApply(s -> s + " World"); // executes in the calling thread!


2. **Unhandled exceptions:**
```java
// ❌ Exception is lost
CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("Error");
}).thenApply(s -> s.toUpperCase());

// ✅ Handling
CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("Error");
}).exceptionally(ex -> "default");

🔴 Senior Level

Internal Implementation

CompletableFuture vs Future:

// Future — interface
public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit) throws ...;
}

// CompletableFuture — implementation + CompletionStage
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
    // CompletionStage provides 40+ methods for composition
}

Internal structure:

// CompletableFuture uses lock-free algorithms
// CAS (Compare-And-Swap) for completion
// Stack of completions/dependencies for chains

class CompletableFuture<T> {
    volatile Object result;  // either value or AltResult (exception)
    // AltResult — internal JDK wrapper class for exceptions.
    // Allows storing null as a valid result and distinguishing it from an exception.
    volatile Completion stack;  // dependency stack

    // CAS for completion
    boolean completeValue(T t) {
        // compareAndSwap for setting result
    }
}

Architectural Trade-offs

Future CompletableFuture
Blocking get() Non-blocking
No error handling Full error handling
Cannot combine thenCombine, allOf, anyOf
Needs ExecutorService Own ForkJoinPool by default

Edge Cases

1. Default Executor:

// CompletableFuture uses ForkJoinPool.commonPool()
// For I/O operations — bad (too few threads)
CompletableFuture.supplyAsync(() -> {
    // I/O operation — need more threads!
    return httpClient.get(url);
});

// ✅ Custom Executor
ExecutorService ioExecutor = Executors.newFixedThreadPool(20);
CompletableFuture.supplyAsync(() -> httpClient.get(url), ioExecutor);

2. Exception propagation:

// Exception is wrapped in CompletionException
CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("Original");
}).thenApply(s -> s.toUpperCase())
  .exceptionally(ex -> {
      // ex — CompletionException with the original cause
      Throwable cause = ex.getCause();  // "Original"
      return null;
  });

Performance

Operation             | Future | CompletableFuture
----------------------|--------|------------------
Creation              | 5 ns   | 10 ns
get() (blocking)      | 100μs+ | 100μs+
thenApply (chain)     | N/A    | 5-10 ns
Error handling        | Manual | Built-in

ForkJoinPool.commonPool():
- Size = availableProcessors - 1
- For CPU-bound tasks — OK
- For I/O — need your own Executor

Production Experience

Async API call:

@Service
public class UserService {
    private final RestTemplate restTemplate;
    private final ExecutorService executor;

    public CompletableFuture<User> getUserAsync(Long userId) {
        return CompletableFuture.supplyAsync(() -> {
            return restTemplate.getForObject(
                "http://api/users/" + userId, User.class
            );
        }, executor);
    }

    // Combining multiple calls
    public CompletableFuture<UserProfile> getProfile(Long userId) {
        CompletableFuture<User> user = getUserAsync(userId);
        CompletableFuture<Order> lastOrder = getLastOrderAsync(userId);

        return user.thenCombine(lastOrder, (u, o) ->
            new UserProfile(u, o)
        );
    }
}

Best Practices

// ✅ Always handle exceptions
cf.exceptionally(ex -> defaultValue);

// ✅ Use your own Executor for I/O
CompletableFuture.supplyAsync(task, ioExecutor);

// ✅ Don't block without necessity
// ❌ cf.get() — only if truly needed

// ❌ Don't ignore CompletableFuture
// ❌ Don't use commonPool for I/O

🎯 Interview Cheat Sheet

Must know:

  • CompletableFuture appeared in Java 8, implements Future + CompletionStage
  • CompletionStage provides 40+ methods for composition
  • ForkJoinPool.commonPool() is used by default (availableProcessors - 1)
  • supplyAsync() — with return value, runAsync() — without
  • Exceptions are wrapped in CompletionException
  • thenApply/thenAccept/thenRun — for chaining, thenCombine/allOf/anyOf — for combining

Frequent follow-up questions:

  • How does it differ from Future? — Future only has blocking get(), CompletableFuture — non-blocking with chains and error handling
  • What pool is used by default? — ForkJoinPool.commonPool(), for I/O need your own Executor
  • How to handle errors? — exceptionally(), handle(), whenComplete()
  • Can you complete manually? — Yes, complete() or completeExceptionally()

Red flags (DO NOT say):

  • “CompletableFuture blocks the thread” — it is non-blocking by design
  • “I use commonPool for HTTP requests” — thread pool starvation
  • “I ignore CompletableFuture without handling” — lost exception

Related topics:

  • [[2. What are the main advantages of CompletableFuture over Future]]
  • [[12. What thread pool is used by default for async methods]]
  • [[6. How to handle exceptions in CompletableFuture chain]]
  • [[16. What does supplyAsync() method do and when to use it]]