Asynchronous Execution
About
While RestTemplate
itself is synchronous by design, it can be used asynchronously by wrapping it with concurrency mechanisms such as CompletableFuture
, ExecutorService
, or integrating with Spring’s @Async
support.
This allows a system to make non-blocking parallel HTTP calls—improving throughput, latency, and resource utilization, especially in IO-bound service-to-service communication.
In modern microservices and cloud-native systems:
Services often call multiple downstream APIs.
Waiting sequentially for all responses can become a performance bottleneck.
Asynchronous execution allows concurrent invocations, reducing overall response time.
It enables use cases like parallel data fetching, timeout-based fallbacks, and circuit breaker integration.
1. Using CompletableFuture with Custom Executor
This is the most popular pattern in enterprise applications for parallel execution.
@Async("customExecutor")
public CompletableFuture<UserResponse> getUserAsync(String userId) {
String url = "http://userservice/api/users/" + userId;
UserResponse response = restTemplate.getForObject(url, UserResponse.class);
return CompletableFuture.completedFuture(response);
}
We must annotate our class with @EnableAsync
and define an executor:
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "customExecutor")
public Executor taskExecutor() {
return new ThreadPoolTaskExecutorBuilder()
.corePoolSize(10)
.maxPoolSize(50)
.queueCapacity(100)
.threadNamePrefix("async-rest-")
.build();
}
}
Then use it:
CompletableFuture<UserResponse> user1 = service.getUserAsync("123");
CompletableFuture<UserResponse> user2 = service.getUserAsync("456");
CompletableFuture.allOf(user1, user2).join();
UserResponse result1 = user1.get();
UserResponse result2 = user2.get();
2. Manual ExecutorService Wrapping
For fine-grained control (without @Async
), wrap the calls manually:
ExecutorService executor = Executors.newFixedThreadPool(10);
Callable<UserResponse> task = () -> restTemplate.getForObject(url, UserResponse.class);
Future<UserResponse> future = executor.submit(task);
Use invokeAll
to parallelize multiple calls.
3. Combining with Retry or Timeout Logic
To prevent indefinite blocking:
CompletableFuture<UserResponse> future = CompletableFuture
.supplyAsync(() -> restTemplate.getForObject(url, UserResponse.class), executor)
.orTimeout(3, TimeUnit.SECONDS)
.exceptionally(ex -> {
log.warn("Timeout or failure for user call", ex);
return fallbackResponse();
});
Last updated