Boost SpringBoot API Performance with CompletableFuture Async Techniques
This article explains how to dramatically reduce SpringBoot endpoint latency by refactoring synchronous REST calls into parallel asynchronous tasks using CompletableFuture, handling thread safety, result aggregation, exception isolation, and custom thread pools for optimal throughput.
1. Introduction
Slow API responses degrade user experience and business efficiency; using multithreading to run independent tasks in parallel can fully utilize system resources and improve overall performance.
2. Interface Optimization
Original synchronous implementation:
@GetMapping("/{id}")
public Map<String, Object> allInfo(@PathVariable("id") Long id) {
Map<String, Object> score = this.restTemplate.getForObject("http://localhost:9001/api/scores/{id}", Map.class, id);
Map<String, Object> order = this.restTemplate.getForObject("http://localhost:9001/api/orders/{id}", Map.class, id);
Map<String, Object> trade = this.restTemplate.getForObject("http://localhost:9001/api/trades/{id}", Map.class, id);
return Map.of("score", score, "order", order, "trade", trade);
}Each downstream call takes 1–2 seconds, resulting in a total latency of about 5 seconds.
First async refactor using CompletableFuture.runAsync:
@GetMapping("/{id}")
public Map<String, Object> allInfo(@PathVariable("id") Long id) {
Map<String, Object> result = new HashMap<>();
CompletableFuture<Void> scoreTask = CompletableFuture.runAsync(() -> {
Map<String, Object> score = this.restTemplate.getForObject("http://localhost:9001/api/scores/{id}", Map.class, id);
result.put("score", score);
});
CompletableFuture<Void> orderTask = CompletableFuture.runAsync(() -> {
Map<String, Object> order = this.restTemplate.getForObject("http://localhost:9001/api/orders/{id}", Map.class, id);
result.put("order", order);
});
CompletableFuture<Void> tradeTask = CompletableFuture.runAsync(() -> {
Map<String, Object> trade = this.restTemplate.getForObject("http://localhost:9001/api/trades/{id}", Map.class, id);
result.put("trade", trade);
});
scoreTask.join();
orderTask.join();
tradeTask.join();
return result;
}This reduces total time to about 2 seconds, as shown in the following result images:
2.1 Further Async Improvements
Thread‑safety: shared result map is modified by multiple threads.
Return type: CompletableFuture<Void> does not convey the fetched data.
Result merging: modifying the map inside child threads mixes concerns.
Improved version using CompletableFuture.supplyAsync and proper result aggregation:
@GetMapping("/{id}")
public Map<String, Object> allInfo(@PathVariable("id") Long id) throws Exception {
CompletableFuture<Map<String, Object>> scoreFuture = CompletableFuture.supplyAsync(() ->
this.restTemplate.getForObject("http://localhost:9001/api/scores/{id}", Map.class, id));
CompletableFuture<Map<String, Object>> orderFuture = CompletableFuture.supplyAsync(() ->
this.restTemplate.getForObject("http://localhost:9001/api/orders/{id}", Map.class, id));
CompletableFuture<Map<String, Object>> tradeFuture = CompletableFuture.supplyAsync(() ->
this.restTemplate.getForObject("http://localhost:9001/api/trades/{id}", Map.class, id));
CompletableFuture.allOf(scoreFuture, orderFuture, tradeFuture).join();
Map<String, Object> result = new HashMap<>();
result.put("score", scoreFuture.get());
result.put("order", orderFuture.get());
result.put("trade", tradeFuture.get());
return result;
}2.2 Exception Isolation
If any downstream call throws, the whole endpoint fails. By adding .exceptionally we can return a placeholder instead of propagating the error:
@GetMapping("/{id}")
public Map<String, Object> allInfo(@PathVariable("id") Long id) throws Exception {
CompletableFuture<Map> scoreFuture = CompletableFuture.supplyAsync(() ->
this.restTemplate.getForObject("http://localhost:9001/api/scores/{id}", Map.class, id))
.exceptionally(ex -> Map.of("data", String.format("接口发生异常: %s", ex.getMessage())));
// similar for orderFuture and tradeFuture (tradeFuture deliberately throws 1/0)
CompletableFuture<Map> tradeFuture = CompletableFuture.supplyAsync(() -> {
System.out.println(1 / 0);
return this.restTemplate.getForObject("http://localhost:9001/api/trades/{id}", Map.class, id);
}).exceptionally(ex -> Map.of("data", String.format("接口发生异常: %s", ex.getMessage())));
CompletableFuture.allOf(scoreFuture, orderFuture, tradeFuture).join();
return Map.of("score", scoreFuture.get(), "order", orderFuture.get(), "trade", tradeFuture.get());
}Now a failure in one service does not affect the others.
2.3 Non‑Blocking Controller
Using Callable lets Tomcat release the request thread while the async tasks run, further improving throughput:
@GetMapping("/{id}")
public Callable<Map> allInfo(@PathVariable("id") Long id) throws Exception {
CompletableFuture<Map> scoreFuture = CompletableFuture.supplyAsync(() ->
this.restTemplate.getForObject("http://localhost:9001/api/scores/{id}", Map.class, id))
.exceptionally(ex -> Map.of("data", String.format("接口发生异常: %s", ex.getMessage())));
// orderFuture and tradeFuture defined similarly
Callable<Map> cb = () -> {
CompletableFuture.allOf(scoreFuture, orderFuture, tradeFuture).join();
return Map.of("score", scoreFuture.get(), "order", orderFuture.get(), "trade", tradeFuture.get());
};
return cb;
}Console logs show the Tomcat thread returns after only a few milliseconds, greatly increasing overall request capacity.
3. Important Considerations
By default CompletableFuture.supplyAsync uses ForkJoinPool.commonPool(). In production you should provide a dedicated ThreadPoolExecutor to control concurrency and avoid resource exhaustion:
private static final ThreadPoolExecutor pool = new ThreadPoolExecutor(18, 18, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000));
@GetMapping("/{id}")
public Callable<Map> allInfo(@PathVariable("id") Long id) throws Exception {
CompletableFuture<Map> scoreFuture = CompletableFuture.supplyAsync(() ->
this.restTemplate.getForObject("http://xxx", Map.class, id), pool)
.exceptionally(ex -> Map.of("data", String.format("接口发生异常: %s", ex.getMessage())));
// ... other futures using the same pool
return cb;
}Specifying a custom pool gives better control over thread count, queue depth, and overall system stability.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
