1. Overview

In this quick tutorial, we’ll learn the differences between CompletableFuture and Mono from Project Reactor in Java. We’ll focus on how they handle asynchronous tasks and the execution that occurs to accomplish those tasks.

Let’s start by looking at CompletableFuture.

2. Understanding CompletableFuture

Introduced in Java 8, CompletableFuture builds upon the previous Future class and provides a way to run code asynchronously. In short, it improves asynchronous programming and simplifies working with threads.

Moreover, we can create a chain of computations with methods like thenApply(), thenAccept(), and thenCompose() to coordinate our asynchronous tasks.

While CompletableFuture is asynchronous, meaning the main thread continues executing other tasks without waiting for the completion of the current operation, it isn’t fully non-blocking. A long-running operation can block the thread executing it:

CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    return "Finished completableFuture";
});

Above, we’re simulating a long operation with the sleep() method from the Thread class. If not defined, supplyAsnc() will use ForkJoinPool and assign a thread to run the anonymous lambda function, and this thread gets blocked by the sleep() method.

Moreover, calling the get() method in the CompletableFuture instance before it completes the operation blocks the main thread:

try {
    completableFuture.get(); // This blocks the main thread
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

To avoid such occurrences, we can handle the CompletableFuture completion manually using the completeExceptionally() or complete() methods in the callback pattern. For example, suppose we have a function that we want to run without blocking the main thread, and then, we want to handle the future when it fails and when it completes successfully:

public void myAsyncCall(String param, BiConsumer<String, Throwable> callback) {
    new Thread(() -> {
        try {
            Thread.sleep(1000);
            callback.accept("Response from API with param: " + param, null);
        } catch (InterruptedException e) {
            callback.accept(null, e);
        }
    }).start();
}

Then, we use this function to create a CompletableFuture:

public CompletableFuture<String> nonBlockingApiCall(String param) {
    CompletableFuture<String> completableFuture = new CompletableFuture<>();
    myAsyncCall(param, (result, error) -> {
        if (error != null) {
            completableFuture.completeExceptionally(error);
        } else {
            completableFuture.complete(result);
        }
    });
    return completableFuture;
}

There’s an alternative and a more reactive way to handle the same operation, as we’ll see next.

3. Comparing Mono With CompletableFuture

The Mono class from Project Reactor uses reactive principles. Unlike CompletableFuture, Mono is designed to support concurrency with less overhead.

Additionally, Mono is lazy compared to the eager execution of the CompletableFuture, meaning that our application won’t consume resources unless we subscribe to Mono:

Mono<String> reactiveMono = Mono.fromCallable(() -> {
    Thread.sleep(1000); // Simulate some computation
    return "Reactive Data";
}).subscribeOn(Schedulers.boundedElastic());

reactiveMono.subscribe(System.out::println);

Above, we’re creating a Mono object using the fromCallable() method and providing the blocking operation as a supplier. Then, we delegate the operation, using the subscribeOn() method, to a separate thread.

Schedulers.boundedElastic() is similar to a cached thread pool but with a limit on the maximum number of threads. This ensures the main thread remains non-blocking. The only way to block the main thread is forcefully call the block() method. This method waits for the completion, successful or not, of the Mono instance.

Then, to run the reactive pipeline, we use subscribe() to subscribe the outcome of the Mono object to println() using method reference.

The Mono class is very flexible and provides a set of operators to transform and combine other Mono objects descriptively. It also supports backpressure to prevent the application from eating up all the resources.

4. Conclusion

In this quick article, we compared CompletableFuture with the Mono class from Project Reactor. First, we described how CompletableFuture can run an asynchronous task. Then, we showed that, if configured incorrectly, it can block the thread it’s working on as well as the main thread. Finally, we showed how to run an asynchronous operation in a reactive way using Mono.

As always, we can find the complete code over on GitHub.