How does CompletableFuture chain asynchronous work in Java, and why is it better than blocking on a Future?
A CompletableFuture is a Future that can be completed by hand and, crucially, that can have continuations attached. Instead of blocking a thread waiting for a result and then starting the next step, you describe the next step as a function and the runtime runs it when the result arrives.
The chain is built from the then-style methods. thenApply transforms a result, thenAccept consumes it and returns another CompletableFuture, thenCompose flattens a nested future, and thenCombine waits for two futures and joins their results. Each returns a new CompletableFuture, so the steps compose into a pipeline.
There are three ways to start the work, and the difference is where the code runs. supplyAsync and runAsync run a supplier on the common ForkJoinPool. completedFuture gives you an already-finished future. And the plain thenApply runs its function on whichever thread completed the previous stage — so if that was an IO thread, your transformation runs on the IO thread too.
That last point is why the Async variants exist. thenApplyAsync moves the continuation onto the executor rather than running it inline, which is what you want when the continuation is slow, or when you want to keep the completing thread free. And you can pass your own executor to thenApplyAsync so the work lands on a pool you control rather than the shared common pool.