multi threading
advanced

CompletableFuture

Compose asynchronous computations into pipelines — like Promises in JavaScript — without blocking on every step.

A CompletableFuture stands for a result that has not arrived yet — the answer to a database query or an HTTP call that is still in flight.

Pizza ordering app — track each pizza's status independently. "When all three are ready, send the delivery driver." No staring at a single oven.

Key Concepts

1
Instead of blocking a thread while you wait, you say what should happen when it arrives.
2
    fetchUser(id)                       // returns immediately
        .thenApply(User::getEmail)      // when the user arrives, take the email
        .thenAccept(this::send);        // then send to it
3
No thread sits idle during any of that.
4
Why it replaced Future. The older Future could only be asked "are you done?" or "wait for you". You could not say what to do next, combine two of them, or attach error handling. CompletableFuture can do all three.
FutureCompletableFuture
5
The method names follow a pattern. Once you see it, the large API shrinks:
6
  thenApply(fn)      the function returns a value        -> transform
  thenAccept(fn)     the function returns nothing        -> consume
  thenRun(r)         ignores the value entirely
  thenCompose(fn)    the function returns ANOTHER future -> flatten
  thenCombine(o,fn)  wait for two futures, combine them
7
thenApply versus thenCompose is the one to get right. If your function itself returns a CompletableFuture and you use thenApply, you end up with a future inside a future:
thenApplythenComposeCompletableFuture
8
    fetchUser(id).thenApply(u -> fetchOrders(u))    // CF<CF<List<Order>>>
    fetchUser(id).thenCompose(u -> fetchOrders(u))  // CF<List<Order>>
9
Which thread runs your code is the trap. Without the Async suffix, the next step runs on whichever thread finished the previous one. That might be a library's internal networking thread, and doing slow work there blocks something you do not own.
Async
10
With Async and no executor, it runs on a pool that is shared by the whole JVM and sized for computation, not waiting. Pass your own executor for anything that blocks.
Async
11
Running calls in parallel, done correctly.
    // sequential by accident -- each join waits before the next starts
    for (var id : ids) results.add(fetch(id).join());
12
    // actually parallel -- all started, then waited for
    var futures = ids.stream().map(this::fetch).toList();
    CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)).join();
    var users = futures.stream().map(CompletableFuture::join).toList();
13
The second join does not block, because those futures are already finished.
join
14
Handling failure. Three tools, with different shapes:
15
  exceptionally(fn)   recover: supply a fallback value
  handle(fn)          runs either way, receives value and error
  whenComplete(fn)    observe both, change nothing
16
Exceptions arrive wrapped in CompletionException, so the real cause is one getCause() away. And a chain that nobody ever waits on can swallow its failure entirely — always finish with a join, a handle, or a whenComplete.
CompletionExceptiongetCause()joinhandlewhenComplete
17
What the interviewer is probing.1. "Why this over Future?" Probing: what it adds. Stalls: "It is newer." Moves up: chaining, combining and error handling without a thread waiting.
Future
18
2. "thenApply or thenCompose?" Probing: the flattening rule. Stalls: guessing. Moves up: thenCompose when the function itself returns a future, or you get a nested one.
thenApplythenCompose
19
3. "Which thread runs the callback?" Probing: the trap. Stalls: "A pool thread." Moves up: without Async, whichever thread completed the previous step — possibly a library's.
Async
20
4. "How do you run ten calls in parallel?" Probing: whether your loop is actually sequential. Stalls: a loop that joins each one. Moves up: start them all, then allOf, then collect.
allOf
21
5. "How do you handle an error?" Probing: the three tools. Stalls: "try/catch." Moves up: exceptionally to recover, handle for both paths, and unwrap CompletionException.
exceptionallyhandleCompletionException
22
  The method grid
23
                     value in ->      returns
  ------------------------------------------------------------------
  thenApply(fn)      T -> R           CF<R>     transform
  thenAccept(c)      T -> void        CF<Void>  consume
  thenRun(r)         () -> void       CF<Void>  ignore the value
  thenCompose(fn)    T -> CF<R>       CF<R>     flatten (flatMap)
  thenCombine(o,fn)  (T,U) -> R       CF<R>     join two stages
  exceptionally(fn)  Throwable -> T   CF<T>     recover
  handle(fn)         (T,Throwable)->R CF<R>     both paths
  whenComplete(c)    (T,Throwable)    CF<T>     observe, unchanged
24
  Each has an ...Async form: same semantics, different thread.
25
  Parallel calls, done correctly
26
    // sequential by accident - each join blocks before the next starts
    for (var id : ids) results.add(fetch(id).join());
27
    // parallel
    List<CompletableFuture<User>> futures =
        ids.stream().map(this::fetch).toList();
    CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new))
                     .join();
    List<User> users = futures.stream()
                              .map(CompletableFuture::join)  // done
                              .toList();
28
  Timeouts (Java 9+)
    orTimeout(3, SECONDS)                 fail after 3 s
    completeOnTimeout(fallback, 3, SECONDS)   default after 3 s
29
  Thread rules
    no Async      -> runs on the completing thread (or the caller,
                     if already complete)
    Async, no exec-> common ForkJoinPool, shared process-wide,
                     sized for CPU work
    Async + exec  -> your pool  <- prefer this for anything blocking