Explain Codes LogoExplain Codes Logo

Completablefuture | thenApply vs thenCompose

java
async-programming
future
java-8
Anton ShumikhinbyAnton Shumikhin·Dec 4, 2024
TLDR

Use thenApply when the transformation of a CompletableFuture's result is synchronous. It's great for direct, snappy transformations.

Opt for thenCompose when you need to marry two stages of asynchronous computation into a smooth workflow. It's your best friend when the next operation also yields a CompletableFuture.

Example:

// thenApply for changing the world (synchronously) CompletableFuture<String> greeting = CompletableFuture.supplyAsync(() -> "Hello").thenApply(name -> name + ", World!"); // thenCompose for changing the world (asynchronously) CompletableFuture<String> asyncGreeting = CompletableFuture.supplyAsync(() -> "Hello").thenCompose(name -> CompletableFuture.supplyAsync(() -> name + ", Async World!"));

Simply put, thenApply gives results quickly, while thenCompose allows you to set up a sequence of events.

The real deal: thenApply vs thenCompose

The thenApply is comparable to map operations in Optional or Stream. It's a handy function for working with the direct result of a CompletableFuture and provides a transformed result encapsulated in a new CompletableFuture. Its like turning water into wine!

On the flip side, thenCompose aligns with flatMap; it enables flattening and chaining operations. When wrestling with a function that yields another CompletableFuture or CompletionStage, it allows you to create a dependent future without sweating over a CompletableFuture<CompletableFuture<T>>.

To put it simply:

  • thenApply: for all your synchronous, transformational needs.

  • thenCompose: for logical next steps involving extra asynchronous operations.

Knowing when to use thenApply or thenCompose

  • thenApply: Utilize this method when you want to sprinkle some transformational magic on the result of the CompletableFuture.

  • thenCompose: When your transformation function also gives you a CompletableFuture that you'd like to flatten, choose this. It's like ordering a flat white instead of a frothy cappuccino.

The synchronous vs asynchronous conundrum

thenApply doesn't handle nesting within CompletableFutures. It's built for those quick-and-simple synchronous transformations.

thenCompose, on the other hand, works wonders for asynchronous operations. It's your knight in shining armor for recursive async operations or an unknown succession of operation chains.

Practical magic

Imagine this: you need to fetch a user and then, based on the user's data, fetch their orders. Sounds familiar?

// Fetch user -> Fetch user's orders. This is asynchronous, like waiting for your food delivery CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> userService.getUser(userId)); CompletableFuture<List<Order>> ordersFuture = userFuture.thenCompose(user -> CompletableFuture.supplyAsync(() -> orderService.getOrders(user)));

Here's the equivalent synchronous scenario with thenApply:

// Fetch user, then transform user to user details string. Like turning a frog into a prince! CompletableFuture<UserDetails> userDetailsFuture = userFuture.thenApply(user -> userService.getUserDetails(user));

Wrestling with nested futures

thenCompose is your best bet when dealing with functions that generate a CompletionStage. It's your secret weapon to avoid nested futures and maintain a clean, linear future.

Consider this nested future predicament:

CompletableFuture<CompletableFuture<String>> nestedFuture = CompletableFuture.supplyAsync(this::getNestedResult).thenApply(name -> CompletableFuture.supplyAsync(() -> getContinuedResult(name)));

To untangle the nested knots, deploy thenCompose:

CompletableFuture<String> flatFuture = CompletableFuture.supplyAsync(this::getFlatResult).thenCompose(name -> CompletableFuture.supplyAsync(() -> getContinuedResult(name)));

Exploring alternatives

The latest Java updates serve other methods like thenCombine and thenAcceptBoth for amalgamating independent futures. These methods are your comrades in arms, handling parallel asynchronous operations that don't vie for each other's results.

In more complex flows, you'll often see multiple thenApply and thenCompose calls chained together:

CompletableFuture<String> complexFlow = CompletableFuture.supplyAsync(() -> "Start") .thenApply(syncStep -> syncStep + " Sync") // Let's get in sync .thenCompose(asyncStep -> CompletableFuture.supplyAsync(() -> asyncStep + " Async")) // Async, that's fantastic! .thenApply(finalStep -> finalStep + " End"); // The End! Or is it?

In this snippet, you see both synchronous and asynchronous steps leading to a calculated result.