
Guide to Java's Future and CompletableFuture
This post uses a practical example to compare two different approaches to asynchronous tasks in Java: Future and CompletableFuture. It also includes the synchronous alternatives so you know the differences when using these asynchronous APIs.
- Asynchronous tasks in Java
- The example use case
- Synchronous tasks in Java
- Java Futures in practice
- CompletableFuture in practice
- Conclusions: Pros and Cons of CompletableFuture
- Advice and Feedback
Asynchronous tasks in Java
This post could dive directly into the technical details of the APIs but then you’d miss an important part. Why should you use asynchronous tasks? Why is CompletableFuture a good fit for that?
Let’s use a practical case here so you can go through different ways of solving the same problem. At the end of this post, you’ll be able to see what are the advantages -and the disadvantages- that the CompletableFuture API brings.
The example use case
The story
We could use an example in which we need to connect to a database, then two external services via REST API and finally aggregate the results. But let’s keep it simple and fun instead: let’s design a bank robbery in Java. You’ll see how this can be easily mapped to real scenarios in which you use database connections, external service calls, etc.
In this story, a group of thieves is planning a robbery of three wealthy villagers who have their money in three safety boxes of the same bank. To get the job done, they need to follow some steps:
- Force the bank's door.
- Find out the safety boxes of the victims. This can be done on a computer inside the bank, only needing the victim's name.
- Figure out the key code (pin) of the victims. These are located in a backup list in the Manager's desk, only needing the names and not the safety box number.
- Having the pin and the safety box number of each target, they just need to open it and get the loot.
- Finally, they'll check the loot and see what they get.
This process is mostly sequential, but steps 2 and 3 can be done in parallel since only the name is needed and this is known in advance. If they don’t do that simultaneously, they’d lose precious time which increases the risk of getting caught.
The Java part
Each of these steps for the robbery is modeled in a class named Actions
. Each step has a delay()
call included, which will apply a sleep time to the calling thread.
package com.thepracticaldeveloper.objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ThreadLocalRandom;
public class Actions {
private static final Logger log = LoggerFactory.getLogger(Actions.class);
private static final long DELAY_MS = 1000L;
private Actions() {
}
public static boolean unlockTheDoor() {
log.info("Forcing the door...");
delay(2000);
log.info("Door unlocked!");
return true;
}
public static int hackSecretPin(final String personName) {
log.info("Hacking the pin of {}", personName);
delay();
final int pin = (personName.hashCode() % 1000) + 1000;
log.info("Pin hacked: {}", pin);
return pin;
}
public static String figureOutSafetyBoxNumber(final String personName) {
log.info("Figuring out the safety box number of {}", personName);
delay();
final String lock = "A" + ThreadLocalRandom.current().nextInt(100);
log.info("Got the safety box number: {}", lock);
return lock;
}
public static Loot openSafeLock(final String safetyBoxNumber, final int pin) {
log.info("Opening the safe lock {} using the pin {}", safetyBoxNumber, pin);
delay();
log.info("Safety Box opened!");
return Loot.randomLoot();
}
private static void delay() {
delay(DELAY_MS);
}
private static void delay(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
We also have a Loot
class available to model the possible outcomes, and a Thief
class to give them a name and make them react to a given type of possible Loot (good, not bad and bad). These classes are really simple, but you can check out the repository if you want to see exactly how they work (and also the possible random loot).
Synchronous tasks in Java
The Imperative Java approach
Let’s solve the problem. One of the thieves doesn’t know too much about concurrency or functional programming, so he designs this old-fashion, imperative-style code, yet a good way of programming in Java:
public Loot openSafeLock(final Thief thief, final String victim) {
Actions.unlockTheDoor();
final String safetyBoxNumber = Actions.figureOutSafetyBoxNumber(victim);
final int pin = Actions.hackSecretPin(victim);
final Loot loot = Actions.openSafeLock(safetyBoxNumber, pin);
log.info("{} gets the content of the safety box: '{}'", thief.getName(), thief.handleLoot(loot));
return loot;
}
This is pretty inefficient to accomplish the plan. The main thread gets blocked every time a delay call is invoked, so we’re not taking the advantage of concurrency at all. We could invoke figureOutSafetyBoxNumber
and hackSecretPin
in a different way so it’s processed at the same time but, instead of that, we’re running them sequentially.
Chaining tasks using Functional Programming
We could also apply the functional APIs introduced with Java 8 and convert the code above into something that looks different. Note that it’s doing exactly the same, and let’s keep the conclusions for the end of the post.
public Loot openSafeLockFunctional(final Thief thief, final String victim) {
return Stream.of(Actions.unlockTheDoor())
.map((ignore) -> Actions.figureOutSafetyBoxNumber(victim))
.map((safetyBoxNumber) -> Actions.openSafeLock(safetyBoxNumber, Actions.hackSecretPin(victim)))
.peek((loot -> log.info("{} gets the content of the safety box: '{}'", thief.getName(), thief.handleLoot(loot))))
.findFirst().get();
}
In this case, we create a Stream
of one element and pipe the results with map
so we can keep our code as small as a single statement (it could be also one line, but a very long one). We get exactly the same result when running this method as we got with the classic Java style:
SINGLE THREAD ====
[INFO] [main] 2018-02-20 06:54:25,753 Forcing the door...
[INFO] [main] 2018-02-20 06:54:27,758 Door unlocked!
[INFO] [main] 2018-02-20 06:54:27,786 Figuring out the safety box number of Mr. Toomuchmoney
[INFO] [main] 2018-02-20 06:54:28,787 Got the safety box number: A84
[INFO] [main] 2018-02-20 06:54:28,787 Hacking the pin of Mr. Toomuchmoney
[INFO] [main] 2018-02-20 06:54:29,798 Pin hacked: 1406
[INFO] [main] 2018-02-20 06:54:29,798 Opening the safe lock A84 using the pin 1406
[INFO] [main] 2018-02-20 06:54:30,813 Safety Box opened!
[INFO] [main] 2018-02-20 06:54:31,834 Lora gets the content of the safety box: 'Lora [When getting the loot 'Oh no, this is a trap! Police is coming.'] : Grrrrr :('
[INFO] [main] 2018-02-20 06:54:31,834 App got the loot BAD
Because of the sequential execution of each step of the process, it takes six seconds to complete the entire flow. We haven’t got yet the solution we’re searching for: hacking the pin and getting the box number simultaneously.
Java Futures in practice
The thieves keep designing the plan and they consider using Java Future
to improve it.

Note how this post doesn’t cover the alternative of using plain Java Threads (implements Runnable
, t.start()
, etc.) to solve this problem. The reason is that its usage is discouraged nowadays: Java has introduced Callable
and Future
just to make our lives easier so it’s better not to go low level for most of the cases.
Back to the Future’s implementation, this is how it looks like:
public Loot openSafeLock(final Thief thief, final String victim) throws Exception {
final ExecutorService executorService = Executors.newFixedThreadPool(4);
final Future<Boolean> doorUnlockFuture = executorService.submit(
Actions::unlockTheDoor
);
doorUnlockFuture.get();
final Future<String> safetyBoxNumberFuture = executorService.submit(
() -> Actions.figureOutSafetyBoxNumber(victim)
);
final Future<Integer> pinRetrieverFuture = executorService.submit(
() -> Actions.hackSecretPin(victim)
);
final Future<Loot> lootFuture = executorService.submit(
() -> Actions.openSafeLock(safetyBoxNumberFuture.get(), pinRetrieverFuture.get())
);
executorService.shutdown();
final Loot loot = lootFuture.get();
log.info("{} gets the content of the safety box: '{}'", thief.getName(), thief.handleLoot(loot));
return loot;
}
The sample code’s explanation
Let’s go through the key concepts of the previous code snippet.
- First, we create an
ExecutorService
, which is a thread pool to invoke our process steps. - We submit the first step as a Java function which is inferred to a
Callable
(remember thatActions::unlockTheDoor
is equivalent to() -> Actions.unlockTheDoor()
, and then we immediately block the main thread waiting for the task to complete (in the use case, we can't get the box number nor hack the pin without forcing the door first). - Then, we create two Futures that will hold the result of two tasks we're sending to the thread pool: getting the safety box number and hacking the pin. Here we finally got the parallelism we wanted: those two threads are running simultaneously while the main thread continues.
- To get the loot we also submit a new thread but note that we need to wait for the arguments to pass them to
openSafeLock
, therefore blocking the thread again via invokingget()
in both Future objects. - Finally, we shut down the thread pool (since we're not sending new threads), wait for the result of the loot Future and pass it to the thief to celebrate it (or not!).
This approach improves our plan: if you run this method, you’ll see how the thieves gain one precious second by getting the pin and the box number in parallel.
PLAIN FUTURES ====
[INFO] [pool-1-thread-1] 2018-02-20 07:27:44,100 Forcing the door...
[INFO] [pool-1-thread-1] 2018-02-20 07:27:46,101 Door unlocked!
[INFO] [pool-1-thread-2] 2018-02-20 07:27:46,102 Figuring out the safety box number of Ms. Greedy
[INFO] [pool-1-thread-3] 2018-02-20 07:27:46,103 Hacking the pin of Ms. Greedy
[INFO] [pool-1-thread-2] 2018-02-20 07:27:47,102 Got the safety box number: A70
[INFO] [pool-1-thread-3] 2018-02-20 07:27:47,103 Pin hacked: 56
[INFO] [pool-1-thread-4] 2018-02-20 07:27:47,103 Opening the safe lock A70 using the pin 56
[INFO] [pool-1-thread-4] 2018-02-20 07:27:48,104 Safety Box opened!
[INFO] [main] 2018-02-20 07:27:49,106 Will gets the content of the safety box: 'Will [When getting the loot 'You've got 1000 Euro!'] : Wooooowww amazing!!'
[INFO] [main] 2018-02-20 07:27:49,106 App got the loot NICE
CompletableFuture in practice
Combining Streams and Futures
The two previous approaches to the solution show the need for something else that can answer these questions:
- The Streams API is great but is not so convenient to perform concurrent actions. Why not applying, for instance, the map-reduce pattern to multiple threads?
- Java Futures simplified the way developers handle threads but it somehow encourages the imperative style. Why not having something like Future, but with the possibility of chaining results and make it a fluent API? Great, that's exactly the question we wanted to trigger.
CompletableFuture
was introduced in Java 8 and received some extra improvements in Java 9. It’s the perfect solution to work with asynchronous tasks in Java: it combines the functional programming style with the power of the promise pattern that Future
implements.
CompletableFuture - Basics
To give some examples from its API, CompletableFuture allows you to:
- Map (or pipe) the result of a Future, by chaining it with another Future that will take its output as argument:
thenApply()
andthenCompose()
(we'll see differences below) - Combine two Future results and then pass them to a new Future, continuing the chain:
thenCombine()
- Terminate with the result of the last Future in the chain (something similar to
forEach()
in a stream of a single element):thenAccept()
- Handle exceptions in a simple way, via providing functions:
exceptionally()
Besides, most of the methods to chain, combine or complete tasks have an asynchronous version (thenApplyAsync
, etc.) in case you want to start a new thread for those steps.
CompletableFuture - Example Code
Let’s see how the plan looks like when we use CompletableFuture
:
package com.thepracticaldeveloper.comparison;
import java.util.concurrent.CompletableFuture;
import com.thepracticaldeveloper.objects.Actions;
import com.thepracticaldeveloper.objects.Loot;
import com.thepracticaldeveloper.objects.Thief;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CompletableFutureOpenSafeLock {
private static final Logger log = LoggerFactory.getLogger(CompletableFutureOpenSafeLock.class);
public Loot openSafeLock(final Thief thief, final String victim) {
return CompletableFuture.supplyAsync(Actions::unlockTheDoor)
.thenCompose(isOpened ->
CompletableFuture.supplyAsync(() -> Actions.figureOutSafetyBoxNumber(victim))
.thenCombineAsync(
CompletableFuture.supplyAsync(() -> Actions.hackSecretPin(victim)),
Actions::openSafeLock
).exceptionally(e -> {
log.error("Something went wrong: {} Run, run, run!!", e.getMessage());
return Loot.BAD;
}
)
).thenApply(
loot -> {
log.info("{} gets the content of the safety box: '{}'", thief.getName(), thief.handleLoot(loot));
return loot;
}
).join();
}
}
The sample code’s explanation
If it’s the first code using CompletableFuture
that you see, it may be overwhelming and hard to follow if presented without explanation. However, when you pay attention to the methods in the chain with some help, everything starts making sense.
Creating CompletableFutures with supplyAsync
The first CompletableFuture
in the chain is created with supplyAsync()
. This is comparable to the submit()
method in Future, with the difference that it returns a CompletableFuture. This is the common denominator of the majority of methods in that class since that’s the way we can chain calls and work with that API in a fluent manner.
Combining Thread Results using CompletableFutures
The splitting point of the simultaneous threads to hack the pin and figuring out the safety box number is done next. The method thenCompose
indicates that we want to execute, after opening the door, another CompletableFuture and gets its completion result (see the box below, that’s the difference between thenCompose
and thenApply
). Let’s see how we can code the execution of these two parallel threads as a continuation of the previous one:
- We create (using supplyAsync) a CompletableFuture that gets the safety box number and then combine it with the CompletableFuture that brings the result of the previous chain (
thenCombineAsync
). - When both tasks are done, we want to execute the action to open the safe lock, passing both results (box number and pin) to the function
openSafeLock
. Thus the second argument ofthenCombineAsync
is that function. If you prefer the longer version, note thatActions::openSafeLock
is equivalent here to(boxNumber, pin) -> Actions.openSafeLock(boxNumber, pin)
.
Exception Handling
As an extra task to show error handling, if something goes wrong in this part of the plan we capture the exception with exceptionally()
and return a result so the process can continue.

Termination
Whenever the nested CompletableFuture is completed, we want to pass the Loot to the thief and see the reaction. To accomplish that, we use the method thenApply
to chain the result of the previous step to a new function that will print the reaction in the console and return the loot.
Waiting for the result
Finally, we call join()
so we wait for the result of the entire block (the loot) before returning it.
When you run this piece this code you see the same result as the one got with the implementation based on Futures. First, the thread to open the door executes; then, two threads to get the box number and hack the pin run in parallel and, once both finish, the results are chained into the thread to open the safe. Finally, when that one ends, the loot is passed to the thief to output its reaction based on the random loot. Is this a better option than the previous one? Check my conclusions below.
CompletableFuture - thenApply vs. thenCompose
In the example, we’re using thenApply()
in the second part of the process to chain a CompletableFuture
result to a synchronous function that returns an object (the loot). However, after the first step (opening the door) we’re using thenCompose()
, why? You can try to change it and see the result. The key is that, when we want to chain an asynchronous task, we flatten its result instead of using the CompletableFuture that thenApply()
would return; otherwise, we would create a <CompletableFuture<CompletableFuture
Knowing when to use one or the other is normally tied to the kind of action you want to chain in the process:
-
Is it a synchronous function, that returns an object? Use
thenApply()
orthenApplyAsync()
if you want to execute it in a separate thread (note that in that case you make it asynchronous). This is somehow equivalent to amap()
operation in a Java Stream. -
Is it a nested CompletableFuture what you're chaining? Use
thenCompose()
orthenComposeAsync()
to chain to the result of the CompletableFuture and not to the CompletableFuture itself. You can see this one as an equivalent to a monadicflatMap()
operation in Streams. Using the async version of the method here is a little bit too much threading though...
Conclusions: Pros and Cons of CompletableFuture
We got a problem to solve in this post, and we managed to present four different solutions to it. If we judge only the result, we can see clearly that the last two are better. They achieve the goal of running two of the actions in parallel, thus getting the same result in less time.
Having a case like this one (as opposed to having just raw, meaningless code) is important because we can balance value and technical implementation and take better decisions, which will be always different depending on the context. Therefore, my first advice is not to go blindly for the CompletableFuture implementation but consider other factors.
Imagine that this use case is just the first of many that are part of a Robbery Simulator Software that will help banks to improve their security. We are the first developers there and we have the above choices. Let’s have a look at the different factors that we might want to consider.
Business Value
The most important factor is the Business Value of what we’re doing. If time was critical when running these scenarios, we should discard the first two options. In that case, we need to make our code use concurrency to complete the simulations in a shorter time. Then, should we implement our code with Futures or CompletableFutures?
Readability and Team Knowledge
This is a controversial subject: is the CompletableFuture implementation easier to read than the Future one? That, as always, depends on who you ask. Some people feel more comfortable with imperative programming and will prefer Futures; some others are Fluent-API lovers that will go for CompletableFutures.
The important factor to consider here is that you won’t be normally coding alone. Check with the team: which option looks simpler to them? In any case, you can always drive the change if needed. If Extensibility (read below) is very important for your project and you’re convinced about the potential of CompletableFuture, but the majority of developers don’t understand its patterns, organize training or workshops to get them onboard.
I recommend you to avoid imposing CompletableFuture in your coding standards (meaning for every piece of code). It’s not an intuitive and easy API, and some people may get frustrated.
Extensibility
In case you envision sophisticated use cases in your project which require a lot of thread composition and proper error handling within the threads, then you should favor CompletableFuture over Future. It gives you a lot of possibilities when you know well its API: not only chaining threads and their results but also embedding error handlers, managing timeouts and even replacing the resulting value (see the hacking behind the obtrudeValue()
javadoc).
Maintainability
You’ll be maintaining the pieces of code you’re creating today for some time, probably. The imperative style may look simpler to maintain, at least to people not very familiar with CompletableFuture. However, if you use proper indentation (please do, for mental sanity) and know where to pause when chaining steps in your process, you can have a really nice, easy-to-maintain code.
Don’t try to apply all possible tricks to your CompletableFuture chain to make it be a long, yet compacted, one-liner. Never forget that you’re not the only person who will need to come back to that magnificent piece of code.
Advice and Feedback
CompletableFuture brings a lot of nice features to Java. It’s a really powerful API and can solve many problems that would cost a lot of effort in previous versions. On the other hand, and as I always recommend, don’t go over-enthusiastic about it. Use it with caution, only if it makes sense for the use case.
I hope you enjoyed this post. Remember that all the source code is available on GitHub and that, if you have any feedback or comments, you can send them via this blog, GitHub issues (question tag), Twitter, etc.

If you want to receive more guides like this one from The Practical Developer, subscribe to the mailing list using the box on the right sidebar. If you like the practical approach used here, have a look at my book about Microservices with Spring Boot: you’ll like it too.
Comments