Backend Development 9 min read

Boost SpringBoot Performance: Parallel REST Calls with CompletableFuture

This tutorial demonstrates how to use Java's CompletableFuture in SpringBoot 2.7.18 to parallelize multiple external REST API calls, covering code implementation, exception handling, and timeout management to improve response time and scalability.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Boost SpringBoot Performance: Parallel REST Calls with CompletableFuture

Environment: SpringBoot 2.7.18

1. Introduction

When developing a project, we often need to fetch data from different interface services and aggregate them into a response. In microservices these data sources are usually external REST APIs. This article shows how to use Java's CompletableFuture to request data from multiple external REST APIs in parallel, and explains exception handling and request timeout.

2. Why Parallel Calls?

Calling APIs sequentially increases overall response time. For example, two APIs each taking 5 seconds would take at least 10 seconds sequentially, but only the longest call (e.g., 7 seconds) when executed in parallel. Parallelization reduces response time, improves scalability and user experience.

3. Practical Example

3.1 Define POJO

<code>public class Purchase {
  private String orderDescription;
  private String paymentDescription;
  private String buyerName;
  private String orderId;
  private String paymentId;
  private String userId;
  // getters and setters
}</code>

The Purchase class has three fields that need to be updated via separate REST calls identified by IDs.

3.2 RestTemplate Bean

<code>@Component
public class PurchaseRestCallsAsyncExecutor {
  private static final String BASE_URL = "http://www.pack.com";
  private final RestTemplate restTemplate;
  public PurchaseRestCallsAsyncExecutor(RestTemplate restTemplate) {
    this.restTemplate = restTemplate;
  }
}</code>

Define three methods for the /orders, /payments and /users APIs, each using restTemplate.getForEntity() and returning the response body.

<code>public String getOrderDescription(String orderId) {
  ResponseEntity<String> result = restTemplate.getForEntity(
    String.format("%s/orders/%s", BASE_URL, orderId), String.class);
  return result.getBody();
}

public String getPaymentDescription(String paymentId) {
  ResponseEntity<String> result = restTemplate.getForEntity(
    String.format("%s/payments/%s", BASE_URL, paymentId), String.class);
  return result.getBody();
}

public String getUserName(String userId) {
  ResponseEntity<String> result = restTemplate.getForEntity(
    String.format("%s/users/%s", BASE_URL, userId), String.class);
  return result.getBody();
}</code>

3.3 Parallel Execution with CompletableFuture

<code>public void updatePurchase(Purchase purchase) {
  CompletableFuture.allOf(
    CompletableFuture.supplyAsync(() -> getOrderDescription(purchase.getOrderId()))
      .thenAccept(purchase::setOrderDescription),
    CompletableFuture.supplyAsync(() -> getPaymentDescription(purchase.getPaymentId()))
      .thenAccept(purchase::setPaymentDescription),
    CompletableFuture.supplyAsync(() -> getUserName(purchase.getUserId()))
      .thenAccept(purchase::setBuyerName)
  ).join();
}</code>

Each supplyAsync creates a task that fetches data, thenAccept stores the result, and allOf().join() runs them concurrently and waits for completion.

3.4 Exception Handling

Use handle to process exceptions for each call.

<code>public void updatePurchaseHandlingExceptions(Purchase purchase) {
  CompletableFuture.allOf(
    CompletableFuture.supplyAsync(() -> getPaymentDescription(purchase.getPaymentId()))
      .thenAccept(purchase::setPaymentDescription)
      .handle((result, exception) -> {
        if (exception != null) {
          // handle exception
          return null;
        }
        return result;
      })
  ).join();
}</code>

3.5 Timeout Control

Apply orTimeout to set a maximum execution time and handle TimeoutException in handle .

<code>public void updatePurchaseWithTimeout(Purchase purchase) {
  CompletableFuture.allOf(
    CompletableFuture.supplyAsync(() -> getOrderDescription(purchase.getOrderId()))
      .thenAccept(purchase::setOrderDescription)
      .orTimeout(5, TimeUnit.SECONDS)
      .handle((result, exception) -> {
        if (exception instanceof TimeoutException) {
          // handle timeout
          return null;
        }
        return result;
      })
  ).join();
}</code>

Setting a timeout prevents threads from waiting indefinitely, improving application health.

It is recommended to provide a custom ExecutorService to supplyAsync for better thread‑pool control.

CompletableFutureSpringBootError HandlingtimeoutJava Concurrencyparallel REST
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.