Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

Java 8’s CompletableFuture is well-suited to handling asynchronous computation. For instance, a web client may employ CompletableFuture when making a server call. It’s easy to get started and handle an individual CompletableFuture response. However, it’s not immediately clear how to collect the results of multiple CompletableFuture executions while also handling exceptions.

In this tutorial, we’ll develop a simple mock microservice client that returns a CompletableFuture, and see how to call it multiple times to generate a summary of successes and failures.

2. An Example Microservice Client

For our example, let’s write a simple microservice client that’s responsible for creating a resource and returning that resource’s identifier.

We’ll declare a simple interface, MicroserviceClient, that we can mock out (using Mockito) in our unit test:

interface MicroserviceClient {
    CompletableFuture<Long> createResource(String resourceName);
}

Unit testing CompletableFuture comes with its own challenges, but testing a single call to MicroserviceClient would be straightforward. Rather than detailing that here, let’s move on to handling multiple client calls that can throw an exception.

3. Combining Multiple Calls to Microservice

Let’s start by creating a unit test and declaring a mock of our MicroserviceClient that returns a successful response for an input of “Good Resource” and throws an exception for an input of “Bad Resource“:

@ParameterizedTest
@MethodSource("clientData")
public void givenMicroserviceClient_whenMultipleCreateResource_thenCombineResults(List<String> inputs,
  int expectedSuccess, int expectedFailure) throws ExecutionException, InterruptedException {
    MicroserviceClient mockMicroservice = mock(MicroserviceClient.class);
    when(mockMicroservice.createResource("Good Resource"))
      .thenReturn(CompletableFuture.completedFuture(123L));
    when(mockMicroservice.createResource("Bad Resource"))
      .thenReturn(CompletableFuture.failedFuture(new IllegalArgumentException("Bad Resource")));
}

We’ll make this a parameterized test and pass in varying sets of data with a MethodSource. We’ll need to create a static method to supply our test with a Stream of JUnit Arguments:

private static Stream<Arguments> clientData() {
    return Stream.of(
      Arguments.of(List.of("Good Resource"), 1, 0),
      Arguments.of(List.of("Bad Resource"), 0, 1),
      Arguments.of(List.of("Good Resource", "Bad Resource"), 1, 1),
      Arguments.of(List.of("Good Resource", "Bad Resource", "Good Resource", "Bad Resource", 
        "Good Resource"), 3, 2)
    );
}

This creates four test executions that pass in a List of inputs and the expected count of successes and failures.

Next, let’s return to our unit test and use the test data to call MicroserviceClient and collect each resulting CompletableFuture into a List:

List<CompletableFuture<Long>> clientCalls = new ArrayList<>();
for (String resource : inputs) {
    clientCalls.add(mockMicroservice.createResource(resource));
}

Now, we have the core part of our problem: a List of CompletableFuture objects that we need to complete and collect the results of, while handling any exceptions we encounter.

3.1. Handling Exceptions

Before getting into how we’ll complete each CompletableFuture, let’s define a helper method for handling exceptions. We’ll also define and mock out a Logger to mimic real-world error handling:

private final Logger logger = mock(Logger.class);

private Long handleError(Throwable throwable) {
    logger.error("Encountered error: " + throwable);
    return -1L;
}

interface Logger {
    void error(String message);
}

The helper method simply “logs” the error message and returns -1, which we’re using to designate an invalid resource.

3.2. Completing a CompletableFuture With Exception Handling

Now, we need to complete all of the CompletableFuture and handle any exceptions appropriately. We can do this by leveraging a few tools CompleteableFuture provides us with:

  • exceptionally(): takes a function to execute if the CompletableFuture completes with an exception
  • join(): returns the result of the CompletableFuture once it completes

Then, we can define a helper method for completion of a single CompletableFuture:

private Long handleFuture(CompletableFuture<Long> future) {
    return future
      .exceptionally(this::handleError)
      .join();
}

Notably, we’re using exceptionally() to handle any exceptions that the MicroserviceClient calls could throw via our handleError() helper method. Finally, we’re calling join() on the CompletableFuture to wait for completion of the client call and return its resource identifier.

3.3. Handling a List of CompletableFuture

Returning to our unit test, we can now leverage our helper methods along with Java’s Stream API to create a simple statement that resolves all of the client calls:

Map<Boolean, List<Long>> resultsByValidity = clientCalls.stream()
  .map(this::handleFuture)
  .collect(Collectors.partitioningBy(resourceId -> resourceId != -1L));

Let’s break down this statement:

  • We map each CompletableFuture into the resulting resource identifier using our handleFuture() helper method
  • We use Java’s Collectors.partitioningBy() utility to split the resulting resource identifiers into separate lists based on validity

We can easily verify our test by using an assertion on the size of the partitioned Lists, as well as checking calls to our mocked Logger:

List<Long> validResults = resultsByValidity.getOrDefault(true, List.of());
assertThat(validResults.size()).isEqualTo(successCount);

List<Long> invalidResults = resultsByValidity.getOrDefault(false, List.of());
assertThat(invalidResults.size()).isEqualTo(errorCount);
verify(logger, times(errorCount))
  .error(eq("Encountered error: java.lang.IllegalArgumentException: Bad Resource"));

Running the test, we can see our partitioned lists match what we expect.

4. Conclusion

In this article, we learned how to handle completing a collection of CompletableFuture. If necessary, we could easily extend our approach to use more robust error handling or complex business logic.

As always, all of the code for the article can be found over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.