Sitemap
Karthick K

Karthick K

CompletableFuture in Java: Asynchronous Programming and Composing Tasks

2 min readOct 28, 2026

--

1. What is CompletableFuture?

CompletableFuture is a Java class that allows us to execute tasks asynchronously, process their results, combine multiple tasks, and handle exceptions without manually managing threads.

It belongs to the java.util.concurrent package and was introduced in Java 8.

Consider a backend API that needs to retrieve three pieces of information to build a user profile:

  • User details
  • Subscription information
  • Notification preferences

If these operations are independent, we can execute them concurrently instead of waiting for one to finish before starting another.

This is one of the most important use cases of CompletableFuture in backend development.

2. Synchronous vs. Asynchronous Execution

Synchronous Execution

User user = fetchUser();
Subscription subscription = fetchSubscription();
Preferences preferences = fetchPreferences();

Each operation starts only after the previous operation finishes.

Suppose the operations take the following time:

  • Fetch user: 300 ms
  • Fetch subscription: 400 ms
  • Fetch preferences: 200 ms

The total execution time is approximately:

300 + 400 + 200 = 900 ms

This assumes the operations execute sequentially and excludes other overhead.

Asynchronous Execution

CompletableFuture<User> userFuture =
CompletableFuture.supplyAsync(() -> fetchUser());
CompletableFuture<Subscription> subscriptionFuture =
CompletableFuture.supplyAsync(() -> fetchSubscription());
CompletableFuture<Preferences> preferencesFuture =
CompletableFuture.supplyAsync(() -> fetchPreferences());

All three tasks are submitted independently. Their execution can overlap, so the total waiting time may approach the duration of the slowest operation.

Get Karthick K’s stories in your inbox

Join Medium for free to get updates from this writer.

In this example, the idealized execution time could approach 400 ms rather than 900 ms.

Asynchronous execution does not automatically guarantee parallel execution. Actual performance depends on the executor, available threads, task type, and resource constraints.

3. runAsync() vs. supplyAsync()

Both methods execute tasks asynchronously, but they differ in whether the task returns a result.

runAsync() — When No Result Is Required

CompletableFuture<Void> future =
CompletableFuture.runAsync(() -> {
System.out.println("Sending notification");
});

Characteristics:

  • Accepts a Runnable.
  • Does not produce a result.
  • Returns CompletableFuture<Void>.
  • Useful for background actions such as sending notifications or writing audit events.

supplyAsync() — When a Result Is Required

CompletableFuture<String> future =
CompletableFuture.supplyAsync(() -> {
return "User details";
});

Characteristics:

  • Accepts a Supplier<T>.
  • Produces a result.
  • Returns CompletableFuture<T>.
  • Useful for retrieving information from databases or external APIs.

The key difference is that Runnable does not return a value, whereas Supplier<T> supplies a value of type T.

Quick comparison:

  • runAsync() — Input: Runnable. Returns: CompletableFuture<Void>.
  • supplyAsync() — Input: Supplier<T>. Returns: CompletableFuture<T>.

--

--