Choosing between thenApply and thenApplyAsync in CompletableFuture

CodesCoddler
3 min readMar 13, 2024

Java’s CompletableFuture class provides two key methods, thenApply and thenApplyAsync, for processing the results of asynchronous computations. While both methods serve the same purpose, their subtle differences can significantly impact the performance and concurrency of your program. This article explores the distinctions between thenApply and thenApplyAsync, offering insights into when to use each based on the specific requirements of your application.

`thenApply` and `thenApplyAsync` are both methods in Java’s CompletableFuture class, and they’re used to process the result of a computation when it becomes available. However, there are key differences between them that can impact the performance and behavior of your program.

1. Execution Thread: The main difference between `thenApply` and `thenApplyAsync` is the thread they’re executed on. `thenApply` executes the callback function on the same thread that completed the previous CompletableFuture, while `thenApplyAsync` executes the callback function on a different thread obtained from the ForkJoinPool.commonPool() (default) or a given Executor.

2. Non-blocking: `thenApplyAsync` is non-blocking. It can offload the subsequent completion stage to some other thread, allowing the current thread to do other work in the meantime. This is beneficial in highly concurrent programs where you want to make the best use of your system’s resources.

3. Performance: In terms of performance, `thenApplyAsync` can be more efficient when the callback function is a long-running task, as it doesn’t block the main thread. However, if the task is short and fast, `thenApply` might be preferable as it avoids the overhead of creating a new task and scheduling it to run on a different thread.

let’s consider a real-world scenario: an online shopping system.

In this system, when a user places an order, several steps occur:

  1. The order details are saved in the database.
  2. An email confirmation is sent to the user.
  3. The order is sent to the warehouse for packaging and delivery.

Each of these steps could be represented by a CompletableFuture, and they could be chained together using thenApply or thenApplyAsync.

Here’s how it might look using thenApply:

CompletableFuture.supplyAsync(() -> saveOrderToDatabase(order))
.thenApply(orderId -> sendConfirmationEmail(orderId))
.thenApply(emailSuccess -> sendOrderToWarehouse(order));

In this case, each step will be executed on the same thread. If sending the confirmation email (sendConfirmationEmail(orderId)) takes a long time, it will block the thread until it's finished, delaying the sendOrderToWarehouse(order) operation.

Now let’s see how it could be improved using thenApplyAsync:

CompletableFuture.supplyAsync(() -> saveOrderToDatabase(order))
.thenApplyAsync(orderId -> sendConfirmationEmail(orderId))
.thenApplyAsync(emailSuccess -> sendOrderToWarehouse(order));

In this case, each step will be executed in a different thread. If sending the confirmation email takes a long time, it will not block the thread that saved the order to the database. Instead, it will be offloaded to a separate thread, and the sendOrderToWarehouse(order) operation can start as soon as the order is saved to the database.

This is a simplified example, but it illustrates the advantage of thenApplyAsync in scenarios where operations could be long-running and you want to avoid blocking threads.

The difference between thenApply and thenApplyAsync comes into play when the CompletableFuture they are chained to is already completed at the time of their invocation.

In the case of thenApply, if the CompletableFuture is already completed, it will run the callback immediately on the invoking thread if it hasn't already started processing the result (i.e., if result processing is not already in progress). This could potentially block the invoking thread if the callback is a long-running operation.

On the other hand, thenApplyAsync will always run the callback in a separate thread from the ForkJoinPool (or a provided Executor), regardless of whether the CompletableFuture is already completed or not. This ensures that the invoking thread will not get blocked even if the callback is a long-running operation.

So, in scenarios where you have a callback that could be a long-running operation and you don’t want to risk blocking the thread that adds the callback, thenApplyAsync would be beneficial. It provides a more consistently asynchronous behavior, ensuring that the operation is always offloaded to a different thread.

--

--