Java Future and CompletableFuture Introduction
Table of Contents
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
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
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.
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:
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
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.
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:
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 (
t.start(), etc.) to solve this problem. The reason is that its usage is discouraged nowadays: Java has introduced
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:
Java Futures: Explained Example
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
Actions::unlockTheDooris 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 invoking
get()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.
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
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:
thenCompose()(we’ll see differences below)
- Combine two Future results and then pass them to a new Future, continuing the chain:
- Terminate with the result of the last Future in the chain (something similar to
forEach()in a stream of a single element):
- Handle exceptions in a simple way, via providing functions:
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 – Explained Example
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
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
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 (
- 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 of
thenCombineAsyncis that function. If you prefer the longer version, note that
Actions::openSafeLockis equivalent here to
(boxNumber, pin) -> Actions.openSafeLock(boxNumber, pin).
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.
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<Loot>> (a promise of a promise, or a nested promise, or whatever fancy name we want to use).
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
thenApplyAsync()if you want to execute it in a separate thread (note that in that case you make it asynchronous). This is somehow equivalent to a
map()operation in a Java Stream.
- Is it a nested CompletableFuture what you’re chaining? Use
thenComposeAsync()to chain to the result of the CompletableFuture and not to the CompletableFuture itself. You can see this one as an equivalent to a monadic
flatMap()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.
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.
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
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.