Implementing Asynchronous Requests in Spring Boot: Callable, WebAsyncTask, and DeferredResult
This article explains how Spring Boot supports asynchronous request handling after Servlet 3.0, compares four async mechanisms—AsyncContext, Callable, WebAsyncTask, and DeferredResult—provides code examples for each, discusses thread‑pool configuration, and outlines when asynchronous processing improves throughput.
Before Servlet 3.0 each HTTP request was processed by a single thread from start to finish. Servlet 3.0 introduced asynchronous processing, allowing the container‑allocated thread to be released early, reducing system load and increasing throughput.
In Spring Boot there are four ways to implement asynchronous endpoints (the article focuses on the three most commonly used):
AsyncContext (low‑level, rarely used)
Callable
WebAsyncTask
DeferredResult
Implementation based on Callable
In a controller, returning a java.util.concurrent.Callable signals an asynchronous endpoint.
@GetMapping("/testCallAble")
public Callable
testCallAble(){
return () -> {
Thread.sleep(40000);
return "hello";
};
}The processing flow is:
Controller returns a Callable .
Spring MVC calls request.startAsync() and submits the Callable to an AsyncTaskExecutor for execution in a separate thread.
The original servlet thread and filters exit, but the response remains open.
When the Callable produces a result, Spring MVC dispatches the request back to the servlet container to complete processing.
The DispatcherServlet is invoked again to handle the asynchronous result.
By default Spring uses SimpleAsyncTaskExecutor , which does not reuse threads; in practice a custom AsyncTaskExecutor should be configured.
Implementation based on WebAsyncTask
WebAsyncTask is a wrapper around Callable that adds timeout, error, and completion callbacks.
@GetMapping("/webAsyncTask")
public WebAsyncTask
webAsyncTask(){
WebAsyncTask
result = new WebAsyncTask<>(30003, () -> "success");
result.onTimeout(() -> {
log.info("timeout callback");
return "timeout callback";
});
result.onCompletion(() -> log.info("finish callback"));
return result;
}The timeout set on WebAsyncTask overrides any global timeout configuration.
Implementation based on DeferredResult
DeferredResult works similarly to Callable but the actual result may be set later from another thread.
// Global map to store DeferredResult objects
private Map
> deferredResultMap = new ConcurrentHashMap<>();
@GetMapping("/testDeferredResult")
public DeferredResult
testDeferredResult(){
DeferredResult
deferredResult = new DeferredResult<>();
deferredResultMap.put("test", deferredResult);
return deferredResult;
}A separate endpoint can later set the result:
@GetMapping("/testSetDeferredResult")
public String testSetDeferredResult() throws InterruptedException {
DeferredResult
deferredResult = deferredResultMap.get("test");
boolean flag = deferredResult.setResult("testSetDeferredResult");
if(!flag){
log.info("Result already processed, operation ignored");
}
return "ok";
}The processing steps are:
Controller returns a DeferredResult and stores it in a shared collection.
Spring MVC calls request.startAsync() and releases the servlet thread while keeping the response open.
Another thread later sets the result via deferredResult.setResult(...) , causing Spring MVC to dispatch the request back for completion.
Both Callable and DeferredResult can have custom timeout values that take precedence over global settings.
Providing a Thread Pool for Asynchronous Requests
@Bean("mvcAsyncTaskExecutor")
public AsyncTaskExecutor asyncTaskExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(10);
executor.setThreadNamePrefix("fyk-mvcAsyncTask-Thread-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
} @Configuration
public class FykWebMvcConfigurer implements WebMvcConfigurer {
@Autowired
@Qualifier("mvcAsyncTaskExecutor")
private AsyncTaskExecutor asyncTaskExecutor;
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
// 0 or negative means never timeout
configurer.setDefaultTimeout(60001);
configurer.setTaskExecutor(asyncTaskExecutor);
}
}When to Use Asynchronous Requests
Asynchronous requests improve throughput only when the request’s business logic spends a lot of time waiting (e.g., calling external services). If the request is CPU‑bound, async adds thread‑switch overhead without benefit.
Because async introduces extra thread switches, it may slightly increase latency, but the gain in overall throughput usually outweighs this cost.
In summary, use async when the server thread would otherwise be idle waiting for I/O, and configure an appropriate thread pool to handle the background work.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
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.