What is the difference between thenApply and thenCompose, and when must you use thenCompose?
answer
- Function returns value → thenApply (map); returns a future → thenCompose (flatMap)
- thenApply on a future-returning fn → CF<CF<T>> nesting
- thenCompose = monadic flatMap, flattens one layer
- IDE shows CF<CF<...>> = you needed thenCompose
basics
~10 sUse thenApply when your function returns a plain value. Use thenCompose when your function itself returns another CompletableFuture, so you chain asynchronous steps without ending up with a CompletableFuture wrapped inside a CompletableFuture.
solid answer
~40 sthenApply takes a Function<T,R> that returns a plain value R, giving you CompletableFuture<R>. thenCompose takes a Function<T, CompletionStage<R>> — a function that returns another future — and flattens it to CompletableFuture<R>. The distinction matters when the next step is itself asynchronous, e.g. lookup a user, then call a remote service that returns CompletableFuture<Order>. If you use thenApply there, R is itself CompletableFuture<Order>, so you get CompletableFuture<CompletableFuture<Order>> — a nested future you'd have to manually unwrap (the old join().join() pain). thenCompose is the monadic flatMap: it subscribes to the inner future and completes the outer stage with the inner's result, so you get a flat CompletableFuture<Order>. Rule of thumb: function returns a value → thenApply (map); function returns a CompletableFuture → thenCompose (flatMap).
code
java · 12 linesCompletableFuture<Long> userId = getUserId();
// WRONG: nested future
CompletableFuture<CompletableFuture<User>> nested =
userId.thenApply(id -> fetchUser(id)); // fetchUser returns CompletableFuture<User>
// RIGHT: flat future via flatMap
CompletableFuture<User> user =
userId.thenCompose(id -> fetchUser(id));
// Recover from an accidental nesting:
CompletableFuture<User> flattened = nested.thenCompose(inner -> inner);go deeper
Can state the rule of thumb: plain value → thenApply, future → thenCompose, and recognizes the nesting problem by name.
Explains map vs flatMap, shows the CF<CF<T>> nesting concretely, and picks correctly for a 'lookup then remote call' pipeline.
Articulates the monadic flatMap analogy, exception/threading semantics, and how Async governs only the composing function, not the inner stage.
Connects it to general monad laws and library design, reasons about composability across CompletionStage implementations, and guides teams away from nested-future anti-patterns in shared code.
## The core problem: async steps that depend on async steps Real pipelines chain operations where **each step may itself be asynchronous**. Suppose: 1. `getUserId()` returns `CompletableFuture<Long>`. 2. `fetchUser(Long)` returns `CompletableFuture<User>` (it calls a remote service). You want a single `CompletableFuture<User>`. How you combine these is where `thenApply` and `thenCompose` diverge. ## thenApply = map (function returns a plain value) `thenApply` has signature `CompletableFuture<R> thenApply(Function<? super T, ? extends R>)`. The function maps `T → R`, a **plain value**. If you naively write `getUserId().thenApply(id -> fetchUser(id))`, the lambda returns a `CompletableFuture<User>`, so `R = CompletableFuture<User>` and the result type becomes: ``` CompletableFuture<CompletableFuture<User>> ``` That is a **nested future**. To get the `User` you'd have to do `.get().get()` or `.join().join()` — blocking, ugly, and error-prone. `thenApply` does **not** look inside the returned value; it just wraps whatever the function returns. ## thenCompose = flatMap (function returns a future) `thenCompose` has signature `CompletableFuture<R> thenCompose(Function<? super T, ? extends CompletionStage<R>>)`. The function maps `T → CompletionStage<R>` (a future). thenCompose **subscribes to that inner future** and completes its own stage with the inner result, **flattening** one layer: ```java CompletableFuture<User> user = getUserId().thenCompose(id -> fetchUser(id)); // flat — no nesting ``` This is exactly the **monadic flatMap** (or `bind`) operation: `map` produces `M<M<R>>`, `flatMap` collapses it to `M<R>`. `CompletionStage` is the interface `CompletableFuture` implements; `thenCompose` accepts any `CompletionStage`, which is why the signature uses it. ## The decision rule - Function returns a **value** → `thenApply` (map). - Function returns a **CompletableFuture / CompletionStage** → `thenCompose` (flatMap). A common smell: if your IDE shows the stage type as `CompletableFuture<CompletableFuture<...>>`, you used `thenApply` where you needed `thenCompose`. ## Threading and exceptions (same as the other transforms) - Both run only on **normal** completion of the upstream; an upstream failure skips the function and propagates. - An exception thrown *inside* the function (or an inner future that completes exceptionally, for `thenCompose`) completes the resulting stage exceptionally. - `thenComposeAsync` runs the **composing function** on the common ForkJoinPool / supplied executor. Note: with `thenCompose`, the *inner* future runs wherever *it* was scheduled — `Async` only governs where your composing lambda is invoked, not the inner stage's own work. ## Analogy with Optional / Stream If you know `Optional` or `Stream`: `map` transforms the contained value; `flatMap` is for when your transform *itself* returns an `Optional`/`Stream` and you want one flat layer. `thenApply` : `thenCompose` :: `map` : `flatMap`.
- What is the concrete type if you call thenApply with a function that returns a CompletableFuture<Order>?CompletableFuture<CompletableFuture<Order>> — a nested future. You'd have to unwrap it manually (e.g. .thenCompose(x -> x), or blocking join().join()). thenCompose avoids the nesting by flattening to CompletableFuture<Order>.
- How would you flatten an accidental CompletableFuture<CompletableFuture<T>> back to CompletableFuture<T>?Call thenCompose with the identity function: nested.thenCompose(inner -> inner). This subscribes to the inner future and completes the outer stage with its result, collapsing the two layers into one.
thenApply is Optional.map / Stream.map; thenCompose is Optional.flatMap / Stream.flatMap. flatMap is what you reach for when the function you apply already returns the same wrapper type, so you don't end up double-wrapped.
saying these in an interview costs you the question
- Saying thenApply 'unwraps' the returned future — it does not; you get nesting.
- Using thenApply for a remote/async call that returns a CompletableFuture.
- Claiming thenCompose can take a function returning a plain value (it must return a CompletionStage).
- Thinking the difference is about threads — it's about value vs future return type (flatten or not).