thenApply vs thenApplyAsync — which thread runs the callb… — Cracked Java
// Concurrency & Multithreading · Callable, Future & CompletableFuture
SeniorTheoryTrickGoogle

thenApply vs thenApplyAsync — which thread runs the callback?

The non-Async variant runs the callback on whatever thread completed the previous stage (or the caller's thread if it is already done); the *Async variant submits the callback to an executor — your own if you pass one, otherwise the ForkJoinPool.commonPool.

The three forms

Every chaining method comes in three flavours:

cf.thenApply(f);                 // runs on the completing thread (or caller)
cf.thenApplyAsync(f);            // runs on ForkJoinPool.commonPool
cf.thenApplyAsync(f, myPool);    // runs on the executor you supply

Who actually runs thenApply (no Async)

This is the trick. With thenApply, the callback executes on the thread that completed the upstream stage — typically the worker that ran supplyAsync. But if the upstream future is already complete when you attach the callback, it runs synchronously on the calling thread.

CompletableFuture<String> done = CompletableFuture.completedFuture("x");
done.thenApply(s -> {
    // already complete -> runs on THIS (main) thread, inline
    return s.toUpperCase();
});

So thenApply's execution thread is non-deterministic from the caller's point of view — it depends on timing. That is fine for cheap transforms, dangerous for blocking work.

Why you would force Async

Two reasons:

  1. Don't hijack the worker. A long or blocking callback on thenApply ties up the upstream pool's thread (often the common pool), starving other tasks. thenApplyAsync hands the work to a dedicated pool.
  2. Control the pool. Always pass your own executor for I/O. The default common pool is sized to cores - 1 and is shared JVM-wide with parallel streams and fork/join.
Executor io = Executors.newFixedThreadPool(32);

CompletableFuture.supplyAsync(this::callServiceA, io)   // on io
    .thenApply(this::cheapParse)                        // on io's worker (cheap, ok)
    .thenApplyAsync(this::callServiceB, io)             // explicitly back on io
    .thenAcceptAsync(this::write, io);

Mark your status