Parallel Requests with Spring WebClient

It covers several ways to make simultaneous HTTP requests with Spring 5 Reactive WebClient and combine their responses with Spring Flux or Java Streams in Parallel.

Overview

When an application wants to make multiple HTTP requests to other services, it does it synchronously or sequentially by default. Although the sequential processing is slow, it is simple as it does not consume too many resources. However, we may want to speed up the process by using simultaneous HTTP calls for performance-critical applications.

This tutorial focuses on executing multiple HTTP calls in-parallel using Spring WebClient. Spring WebClient is an HTTP Client introduced in the Spring WebFlux project. We can use it flexibly in a reactive non-blocking or a simple blocking fashion.

If you are new to Spring 5 Reactive WebClients, we recommend reading Spring WebClient Guide.

HTTP Endpoint

Consider our application wants to fetch multiple Dog resources from an imaginary dog service. The endpoint on the upstream service looks like this.

@GetMapping("/{id}")
public Dog getById(@PathVariable Long id)
    throws InterruptedException {
  TimeUnit.of(ChronoUnit.SECONDS).sleep(3);
  return service.getDogById(id);
}Code language: Java (java)

The GET endpoint on this service returns a Dog resource based on the provided id. Note that we have added a sleep time of 3 seconds before the return statement. The delayed response will help us to observe the sequential and parallel behaviors of our Spring WebClient.

To invoke this endpoint, we will use Spring WebClient.

private Mono<Dog> getDog(Long id) {
  return webClient()
    .get()
    .uri("/dogs/{id}", id)
    .retrieve()
    .bodyToMono(Dog.class);
}


private WebClient webClient() {
  return WebClient.builder()
    .baseUrl("http://localhost:8080/")
    .build();
}Code language: Java (java)

Now that we have our upstream service and a client application that uses Spring WebClient to invoke the service endpoint, we will see different approaches to invoke multiple calls on the service.

Sequential WebClient Calls

Quickly, we will see an example of using Spring WebClient to make multiple HTTP requests sequentially.

To Download multiple resources, we can use Java Streams.

public void getInSequence(Long... ids) {
  Arrays.stream(ids)
    .map(this::getDog)
    .map(d -> d.share().block())
    .forEach(d -> log.info(d.toString()));
}Code language: Java (java)

Above is a simple Java Steam pipeline, where we are fetching the resources and printing them in the end. As the processing of Java Stream elements happens sequentially, The Stream pipeline will execute our Dog service in sequence.

20:15:44 - Fetching dogs sequentially
20:15:47 - Dog(id=1, name=Benji, age=10)
20:15:50 - Dog(id=3, name=Brinkley, age=8)
20:15:53 - Dog(id=4, name=Daisy, age=10)
20:15:56 - Dog(id=2, name=Baxter, age=9)

From the logs, it is clear that the pipeline executed all the requests in sequence. Thus it took around 12 seconds to fetch all the dogs.

Simultaneous WebClient Calls with Java Parallel Streams

Java Streams support sequential as well as parallel execution of its elements. In a parallel stream, all the pipeline operations happen simultaneously.

public void getInParallel(Long... ids) {
  Arrays.stream(ids)
    .parallel()
    .map(this::getDog)
    .map(d -> d.share().block())
    .forEach(d -> log.info(d.toString()));
}Code language: Java (java)

First, we created a Stream of Dog ID and then converted the Stream to a parallel stream. The rest of the stream pipeline is the same as in the previous example.

20:16:06 - Fetching dogs in parallel using Java Streams
20:16:09 - Dog(id=3, name=Brinkley, age=8)
20:16:09 - Dog(id=2, name=Baxter, age=9)
20:16:09 - Dog(id=1, name=Benji, age=10)
20:16:09 - Dog(id=4, name=Daisy, age=10)

The Spring WebClient fetched executed all the HTTP requests simultaneously – in parallel.

Simultaneous WebClient Calls with Spring WebFlux

Similarly, we can use Spring Flux publisher to make simultaneous HTTP calls to a service.

public void getInParallel(Long... ids) {
  Flux.fromArray(ids)
    .parallel()
    .flatMap(this::getDog)
    .ordered((d1, d2) -> (int) (d1.getId() - d2.getId()))
    .toStream() 
    .forEach(d -> log.info(d.toString()));
}Code language: Java (java)

First, we created a Flux of all the Id elements. Next, we created an instance of ParallelFlux by calling the parallel() method on the Flux. ParallelFlux can process all elements in parallel.

Next, we converted the ParallelFlux containing the Dog instances to a Flux by ordering the elements by their Id field. Lastly, we converted the Flux into Stream and printed all the elements.

20:16:13 - Fetching dogs in parallel using Flux
20:16:16 - Dog(id=1, name=Benji, age=10)
20:16:16 - Dog(id=2, name=Baxter, age=9)
20:16:16 - Dog(id=3, name=Brinkley, age=8)
20:16:16 - Dog(id=4, name=Daisy, age=10)

As we can see, we executed all four Spring WebClient HTTP requests in parallel.

Simultaneous WebClient calls with Flux and Scheduler

To have more control over the amount of parallelism, we can also use Scheduler. With this, we can configure asynchronous boundaries using ExecutorService, or ScheduledExecutorService implementations.

public void getInParallel(Long... ids) {
  Flux.fromArray(ids)
    .parallel()
    .runOn(Schedulers.boundedElastic())
    .flatMap(this::getDog)
    .ordered((o1, o2) -> (int) (o1.getId() - o2.getId()))
    .toStream()
    .forEach(d -> log.info(d.toString()));
}
Code language: Java (java)

Here, we provided a bounded elastic type of Scheduler to run these tasks on. For more details on the various implementations of the Scheduler interface, refer to Schedulers.

Simultaneous WebClient calls to Different Services

We executed parallel HTTP requests to the same endpoint in the previous examples. In some cases, we may want Spring WebClient to call different HTTP Services returning the same type simultaneously.

In the following example, we use Spring WebClient to fetch our resources from two different services in parallel.

public void getInParallel(Long... ids) {
  Flux.merge(
      getDog(ids[0]),
      getDog(ids[1]),
      getOtherDog(ids[2]),
      getOtherDog(ids[3]))
    .parallel()
    .ordered((o1, o2) -> (int) (o1.getId() - o2.getId()))
    .toStream()
    .forEach(d -> log.info(d.toString()));
}Code language: Java (java)

Instead of creating Flux of the Ids, we have made Flux of Dog instances returned from two different services. Next, we created a ParallelFlux and obtained and ordered Flux.

Simultaneous WebClient calls to Fetch and Zip different Types

In real-life scenarios, we may want Spring WebClient to make parallel calls to different endpoints returning different types and combine their results.

Let us assume that we have to bring different information from different services to compose a piece of data. Once we receive all the responses, we must combine them to prepare the intended object. We can make this process faster if we interact with different services in parallel.

To make the parallel calls to different services, we will use Spring WebClient and use Mono#zip() method to merge two other response objects to get a combined object.

public void getInParallelAndMerge(Long... ids) {
  Arrays.stream(id)
    .map(i -> {
      Scheduler scheduler = Schedulers.boundedElastic();
      Mono<Dog> dogMono = getDog(i).subscribeOn(scheduler);
      Mono<String> ownerMono = getOwner(i).subscribeOn(scheduler);
      return Mono.zip(dogMono, ownerMono, OwnedDog::new).block();
    })
    .forEach(ownedDog -> log.info(ownedDog.toString()));
}Code language: Java (java)

For each Id we declared Mono instances of getDog() and getOwner() responses. Then we used the zip() method to create a combined Mono containing the OwnedDog instances.

Note that we have used the subscribeOn() method on both Mono instances, and we have provided reference to our Scheduler. That ensures that when the Mono instances are subscribed – inside the zip() method – they are processed in parallel.

20:16:18 - Fetching different resources in parallel from different services using Flux
20:16:21 - OwnedDog(id=1, name=Benji, age=10, owner=Benji's Owner)
20:16:24 - OwnedDog(id=3, name=Brinkley, age=8, owner=Brinkley's Owner)
20:16:27 - OwnedDog(id=4, name=Daisy, age=10, owner=Daisy's Owner)
20:16:30 - OwnedDog(id=2, name=Baxter, age=9, owner=Baxter's Owner)

Notice that each of the Dog ids is processed 3 seconds apart. That is because we did not use parallel Stream here. However, the associated Dog and Owner are fetched simultaneously for each id.

Summary

This tutorial covered several ways of executing simultaneous HTTP Requests using Spring 5 Reactive WebClient. First, we used Java Parallel Streams to invoke the Reactive WebClient requests in parallel. Next, we used Spring Reactive ParallelFlux to make parallel API calls. We also explored ways of configuring and scheduling the worker threads for Flux.

Next, we covered invoking different service APIs returning the same type in parallel. In the last part, we invoked multiple HTTP requests in parallel to download objects of different types and combined them using Mono.zip() method.

Refer to our Github Repository for the complete source code of the examples used in this tutorial.