Call Spring REST APIs Concurrently Using Java Completable Future

REST Services

In this tutorial I am going to show you how to call Spring REST APIs concurrently using Java CompletableFuture. So basically REST service APIs will be invoked parallelly and in parallel execution the response time will be very less. I am going to show you how to call REST APIs concurrently using Java 8 or later’s new feature CompletableFuture.

In microservices environment it makes sense to make multiple service calls concurrently or rather it sometimes becomes an unavoidable situation where you need to make some calls to multiple services at the same time. Making concurrent calls to services are good ideas because it will reduce the time required to complete the whole operation rather than spending the sum of time spent over the span of all calls.

For example, let’s say you make three calls in one service, and let’s further say that all three services can be called in any order. Let’s say each of these services take the time to respond to the client or caller as given below:

Service Call #1 takes 400ms
Service Call #2 takes 600ms
Service Call #3 takes 500ms

Total time taken by all three services to respond to the caller is 400 + 600 + 500 = 1500 ms. However if you make all three service calls concurrently or at the same time and wait for them to complete, then you have to incur the cost of waiting for the service that takes longest time. In this case, the Service Call #2 takes the longest time to complete and you have to wait maximum of 600 ms.

For this example, I will create a simple spring boot application that simulates a long running process.

call spring rest apis concurrently using java completablefuture

Prerequisites

Java 8/19, Spring Boot 2.5.3/3.1.09, Maven 3.8.1/3.8.5

Service API

I am creating a spring boot application with simple REST API that will return just a Hello string in the response in JSON format.

In the REST API method sayHello(), I have put Thread.sleep(1000) to make each call waiting for 1000 ms to understand whether multiple parallel or concurrent calls to this service are happening or not. So for sequential calls the total time will be spent as number of calls x 1000. But for concurrent calls it should be very less because the tasks get executed parallelly instead of sequentially.

@RestController
@SpringBootApplication
public class ServiceApiApp {

	public static void main(String[] args) {
		SpringApplication.run(ServiceApiApp.class, args);
	}

	@GetMapping("/msg")
	public ResponseEntity<String> sayHello() {
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return new ResponseEntity<String>("Hello", HttpStatus.OK);
	}

}

Concurrent Calls

Now I am going to create another spring boot application to consume the above service API. I will make few calls to the above service and will wait for all of them to complete before returning.

I will use RestTemplate to call the above REST service.

Thread Pool Config

I will use executor from Spring’s @Async feature to run the tasks in background thread pool.

@EnableAsync
@Configuration
public class AsyncConfig implements AsyncConfigurer {

	@Override
	public Executor getAsyncExecutor() {
		ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
		executor.setCorePoolSize(10);
		executor.setMaxPoolSize(25);
		executor.setQueueCapacity(100);
		executor.initialize();
		return executor;
	}

	@Override
	public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
		return new CustomAsyncExceptionHandler();
	}

}

The @EnableAsync annotation switches on Spring’s ability to run @Async methods in a background thread pool.

ThreadPoolTaskExecutor – the main idea is that when a task is submitted, the executor will first try to use a free thread if the number of active threads is currently less than the core size. If the core size has been reached, then the task will be added to the queue as long as its capacity has not yet been reached. Only then, if the queue’s capacity has been reached, will the executor create a new thread beyond the core size. If the maximum size has also been reached, then the executor will reject the task.

In the above config class I have used core thread pool size is 10 and maximum thread pool size is 25.

RestTemplate Config

The following bean instance is used to call the REST service.

@Configuration
public class BeanConfig {

	@Bean
	public RestTemplate restTemplate() {
		return new RestTemplate();
	}

}

Service

The following service calls the REST service (having endpoint /msg).

@Service
public class AsyncService {

	@Autowired
	private RestTemplate restTemplate;

	@Async
	public CompletableFuture<String> callMsgService() {
		final String msgServiceUrl = "http://localhost:8080/msg";

		final String response = restTemplate.getForObject(msgServiceUrl, String.class);

		return CompletableFuture.completedFuture(response);
	}

}

In the above configuration, Spring will inject a proxy every time AsyncService.callMsgService() is called. Actually, the previously defined Executor is responsible for executing the calls. As long as you return a CompletableFuture, this network call doesn’t really matter what you do and you can run a database query, a compute-intensive process using in memory data, or any other long running process.

Making Calls

Now I will implement CommandLineRunner to make multiple calls.

@SpringBootApplication
public class ConcurrentRunnerApp implements CommandLineRunner {

	@Autowired
	private AsyncService asyncService;

	@Autowired
	private ConfigurableApplicationContext context;

	public static void main(String[] args) {
		SpringApplication.run(ConcurrentRunnerApp.class, args);// .close();
	}

	@Override
	public void run(String... args) throws Exception {
		Instant start = Instant.now();

		List<CompletableFuture<String>> allFutures = new ArrayList<>();

		for (int i = 0; i < 10; i++) {
			allFutures.add(asyncService.callMsgService());
		}

		CompletableFuture.allOf(allFutures.toArray(new CompletableFuture[0])).join();

		for (int i = 0; i < 10; i++) {
			System.out.println("response: " + allFutures.get(i).get().toString());
		}

		Instant finish = Instant.now();

		long timeElapsed = Duration.between(start, finish).toMillis();

		System.out.println("Total time: " + timeElapsed + " ms");

		System.exit(SpringApplication.exit(context));
	}

}

Notice inside the run() method how I am making multiple calls to the same service API. I am using ConfigurableApplicationContext to close the spring boot application completely.

Testing the Concurrent Calls to REST APIs

Make sure you run the service API application (ServiceApiApp class) first followed by the client (consumer) application (ConcurrentRunnerApp class).

When you run the ConcurrentRunnerApp class, you will see the following output:

response: Hello
response: Hello
response: Hello
response: Hello
response: Hello
response: Hello
response: Hello
response: Hello
response: Hello
response: Hello
Total time: 1074 ms

Total time taken by concurrent calls is 1074 ms and concurrent calls were executed in 10 threads.

Now if you change the core pool size in executor from setCorePoolSize(10); to setCorePoolSize(5); and re-run the ConcurrentRunnerApp class, then you will see the total time taken by service calls has been double.

response: Hello
response: Hello
response: Hello
response: Hello
response: Hello
response: Hello
response: Hello
response: Hello
response: Hello
response: Hello
Total time: 2079 ms

Therefore the number of threads is less than the number tasks, then tasks have to be waiting in the queue for threads to be available for executing. So for sequential execution of tasks will take more time than parallel or concurrent execution of tasks.

Source Code

Download

1 thought on “Call Spring REST APIs Concurrently Using Java Completable Future

Leave a Reply

Your email address will not be published. Required fields are marked *