How to Make Multiple Rest API Calls in Parallel

Recently, I was given a task in which I had to call the OMDB API to fetch the box office value of 10 top-rated movies on the fly. Making 10 REST API calls synchronously and combining the results would definitely be a time-consuming task. Hence I thought of making parallel API calls using CompletableFuture provided by the java.util.concurrent package.

Table of Contents

  1. Introduction to Future
    1. Limitations
    2. CompletableFuture
  2. Synchronous vs Asynchronous Rest API Calls
  3. Get vs Join
  4. RunAsync vs SupplyAsync
  5. Conclusion

Introduction to Future

Future represents the result of an asynchronous computation. Methods are provided to check if the computation is complete, wait for its completion, and retrieve the result. The result can only be retrieved using the method get when the computation has been completed, blocking if necessary until it is ready. Cancellation is performed by the cancel method.

Limitations

  • It cannot be manually completed by setting its value and status
  • No further action can be performed on a Future’s result without blocking,
  • Multiple futures cannot be combined together and then run some function after all of them are complete.
  • Multiple futures cannot be chained in order to send the result of one Future to another after the completion.
  • It does not have any exception-handling construct.

CompletableFuture

CompletableFuture was introduced as an extension of Future API in order to overcome its limitations. It is a Future that may be explicitly completed (setting its value and status) and may be used as a CompletionStage, supporting dependent functions and actions that trigger upon its completion.

Synchronous vs Asynchronous Rest API Calls

Let’s see the example in which we will make multiple REST API calls in synchronous and asynchronous methods and see the performance difference.

@Override
    public List<MovieDTO> findTop10RatedMovies() {
        List<Movie> movies = movieRepository.findTop10RatedMovies(PageRequest.of(0, 10));
        
        // Asynchronous execution
        Executor executor = Executors.newFixedThreadPool(10);
        long start = System.currentTimeMillis();
        var futureMovies = movies.stream().map(m -> CompletableFuture.supplyAsync(() ->
                omdbService.enrichMovieWithBoxOfficeValue(m), executor)).collect(toList());
        var topMovies = futureMovies.stream().map(CompletableFuture::join).collect(toList());
        long end = System.currentTimeMillis();
        System.out.printf("The future operation took %s ms%n", end - start);
        
        // Synchronous execution
        start = System.currentTimeMillis();
        topMovies = movies.stream().map(m -> omdbService.enrichMovieWithBoxOfficeValue(m)).collect(Collectors.toList());
        end = System.currentTimeMillis();
        System.out.printf("The normal operation took %s ms%n", end - start);
        
        topMovies.sort((o1, o2) -> o2.getBoxOffice().compareTo(o1.getBoxOffice()));
        return topMovies;
    }

Output

The future operation took 580 ms
The normal operation took 2221 ms

As you can see, the synchronous execution took 4X more time compared to asynchronous execution. I have used CompletableFuture instead of Future for the asynchronous execution Since I wanted to combine multiple futures together and perform the sorting after all of them are complete which is not possible with the latter.

Now, let’s see in detail to understand how the REST API calls are being made parallelly. Firstly, we need to create a fixed thread pool for asynchronous execution.

 Executor executor = Executors.newFixedThreadPool(10);

Secondly, we must create a list of CompletableFuture objects for each movie. Hence, we are creating a stream of movies and then creating a CompletableFuture object for each movie with the help of CompletableFuture.supplyAsync() method.

CompletableFuture.supplyAsync(Supplier supplier, Executor executor) method returns a new CompletableFuture that is asynchronously completed by a task running in the given executor with the value obtained by calling the given Supplier.

It expects the following parameters:

  • supplier – a function returning the value to be used to complete the returned CompletableFuture
  • executor – the executor to use for asynchronous execution
 var futureMovies = movies.stream().map(m -> CompletableFuture.supplyAsync(() ->
                omdbService.enrichMovieWithBoxOfficeValue(m), executor)).collect(toList());

Note 1: If you don’t want to create a thread pool, then you can use the other variant of CompletableFuture.supplyAsync() method which expects only the Supplier. It returns a new CompletableFuture that is asynchronously completed by a task running in the ForkJoinPool.commonPool() with the value obtained by calling the given Supplier.

Note 2: If you want to execute asynchronous tasks that don’t return anything, then you can use CompletableFuture.runAsync() method. We will explore the differences between these 2 in the next section.

Finally, we need to call the join method of CompletableFuture which returns the result value when complete. Then the results can be collected into a list.

var topMovies = futureMovies.stream().map(CompletableFuture::join).collect(toList());

Note: You can also use the CompletableFuture.get() that waits if necessary for the computation to complete and then retrieves its result.

Get vs Join

  1. The get method is from the Future interface while the join method is from CompletableFuture.
  2. The get throws checked exceptions InterruptedException and ExecutionException which needs to be handled explicitly while join throws an Unchecked exception.
  3. There is also one more variant of get which takes wait time as an argument and waits for at most the provided wait time. However, this is not supported by the join method.

RunAsync vs SupplyAsync

  • runAsync takes Runnable as an input parameter and returns CompletableFuture<Void>, which means it does not return any results while supplyAsync takes the Supplier as an argument and returns the CompletableFuture<U> with result value.
  • If you want the result to be returned, then use supplyAsync or if you just want to run an async action, then use runAsync method.

Conclusion

That’s all folks. In this article, we have seen how to make Multiple Rest API Calls in Parallel using CompletableFuture.

Thank you for reading.

Leave a Reply