Asynchronous Request Handling in Spring Boot: Callable, WebAsyncTask, and DeferredResult
This article explains how to implement asynchronous request handling in Spring Boot using Callable, WebAsyncTask, and DeferredResult, detailing their processing flow, configuration of thread pools, and scenarios where async requests improve throughput while outlining potential trade‑offs.
Before Servlet 3.0 each HTTP request was processed from start to finish by a single thread. After Servlet 3.0 asynchronous processing allows the container thread to be released early, reducing system load and increasing throughput.
In a Spring Boot application there are four ways to implement asynchronous endpoints (ResponseBodyEmitter, SseEmitter, StreamingResponseBody are omitted here). This article focuses on three: Callable , WebAsyncTask , and DeferredResult .
Callable implementation
@GetMapping("/testCallAble")
public Callable
testCallAble(){
return () -> {
Thread.sleep(40000);
return "hello";
};
}The controller returns a Callable which Spring MVC submits to an AsyncTaskExecutor . The original servlet thread and filters exit while the response remains open. Once the Callable produces a result, the request is dispatched back to the servlet container to complete.
By default Spring uses SimpleAsyncTaskExecutor , which does not reuse threads; in practice a custom AsyncTaskExecutor (e.g., a thread pool) should be provided.
WebAsyncTask implementation
@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;
}WebAsyncTask wraps a Callable and adds timeout, error, and completion callbacks. The timeout configured here overrides any global timeout setting.
DeferredResult implementation
// 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;
}
@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 controller returns a DeferredResult that is stored in a map. Another endpoint later sets the result, causing the pending request to complete. It is important to clean up expired or processed DeferredResult instances (e.g., using DeferredResult.isSetOrExpired() ) to avoid memory leaks.
Providing a custom thread pool
@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){
configurer.setDefaultTimeout(60001);
configurer.setTaskExecutor(asyncTaskExecutor);
}
}The custom executor is injected into Spring MVC's async support configuration, allowing asynchronous requests to run on a dedicated thread pool.
When to use asynchronous requests
Async requests improve throughput when the request spends most of its time waiting (e.g., calling external services) because the servlet thread can be released for other work. They are less useful for CPU‑bound tasks that keep the thread busy, and they add a small overhead due to thread switching.
In summary, Spring Boot provides flexible mechanisms— Callable , WebAsyncTask , and DeferredResult —to handle long‑running or I/O‑bound operations asynchronously, and proper thread‑pool configuration is essential to reap performance benefits.
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
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.