Guide to Java’s Future and CompletableFuture

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

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:

  1. Force the bank's door.
  2. Find out the safety boxes of the victims. This can be done on a computer inside the bank, only needing the victim's name.
  3. 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.
  4. Having the pin and the safety box number of each target, they just need to open it and get the loot.
  5. 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.

CompletableFuture Introduction

The Java part

All the code in this post is available on GitHub: CompletableFuture Example. If you find it useful, please give it a star!

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.

Get the book Practical Software Architecture

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 that Actions::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 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.

 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() and thenCompose() (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 of thenCombineAsync is that function. If you prefer the longer version, note that Actions::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.

Get the book Practical Software Architecture

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></code> (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:

  1. Is it a synchronous function, that returns an object? Use thenApply() or 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.
  2. Is it a nested CompletableFuture what you're chaining? Use thenCompose() or 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.

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.

Get the book Practical Software Architecture

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.

Moisés Macero's Picture

About Moisés Macero

Software Developer, Architect, and Author.
Do you need help?

Amsterdam, The Netherlands https://thepracticaldeveloper.com