Implementing Asynchronous Requests in Spring Boot: Callable, WebAsyncTask, and DeferredResult
This article explains how to implement asynchronous request handling in Spring Boot using four approaches—AsyncContext, Callable, WebAsyncTask, and DeferredResult—detailing their mechanisms, code examples, thread pool configuration, and when to choose async processing to improve throughput.
Introduction
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 thread to be released early, reducing system load and increasing overall throughput.
Asynchronous Options in Spring Boot
Spring Boot offers four ways to implement asynchronous endpoints (the ResponseBodyEmitter , SseEmitter and StreamingResponseBody approaches are omitted from this article): AsyncContext , Callable , WebAsyncTask , and DeferredResult .
Callable
When a controller method returns a java.util.concurrent.Callable , Spring MVC treats the request as asynchronous. The flow is:
Spring MVC invokes request.startAsync() and submits the Callable to an AsyncTaskExecutor .
The original servlet thread and all filters exit, while the response remains open.
After the Callable finishes, Spring MVC dispatches the request back to the servlet container to complete processing.
The result is then returned to the client just like a synchronous endpoint.
@GetMapping("/testCallAble")
public Callable<String> testCallAble() {
return () -> {
Thread.sleep(40000);
return "hello";
};
}WebAsyncTask
WebAsyncTask is a wrapper around Callable that adds callbacks for timeout, error, and completion. It is usually preferred over raw Callable because of these extra features.
@GetMapping("/webAsyncTask")
public WebAsyncTask<String> webAsyncTask() {
WebAsyncTask<String> result = new WebAsyncTask<>(30003, () -> "success");
result.onTimeout(() -> {
log.info("timeout callback");
return "timeout callback";
});
result.onCompletion(() -> log.info("finish callback"));
return result;
}The timeout configured on WebAsyncTask overrides any global async timeout settings.
DeferredResult
DeferredResult works similarly to Callable but the actual result may be set later from another thread. A typical pattern is to store the DeferredResult in a map, allowing other components to complete the request when the data becomes available.
private Map<String, DeferredResult<String>> deferredResultMap = new ConcurrentHashMap<>();
@GetMapping("/testDeferredResult")
public DeferredResult<String> testDeferredResult() {
DeferredResult<String> deferredResult = new DeferredResult<>();
deferredResultMap.put("test", deferredResult);
return deferredResult;
}
@GetMapping("/testSetDeferredResult")
public String testSetDeferredResult() throws InterruptedException {
DeferredResult<String> deferredResult = deferredResultMap.get("test");
boolean flag = deferredResult.setResult("testSetDeferredResult");
if (!flag) {
log.info("Result already processed, operation ignored");
}
return "ok";
}When the result is set, the pending request is completed and the client receives the value. It is also possible to configure a custom timeout for a DeferredResult , which takes precedence over the global timeout.
Thread‑Pool Configuration
Asynchronous processing should use a dedicated thread pool instead of the default SimpleAsyncTaskExecutor . Example bean definition:
@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;
}And register it in MVC configuration:
@Configuration
public class FykWebMvcConfigurer implements WebMvcConfigurer {
@Autowired
@Qualifier("mvcAsyncTaskExecutor")
private AsyncTaskExecutor asyncTaskExecutor;
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
configurer.setDefaultTimeout(60001);
configurer.setTaskExecutor(asyncTaskExecutor);
}
}When to Use Asynchronous Requests
Async requests improve throughput only when the request spends a significant amount of time waiting (e.g., calling external services) rather than performing CPU‑bound work. In CPU‑intensive scenarios moving the work to another thread does not gain performance and may even add overhead due to thread switching.
Because async processing introduces extra thread switches, the latency of an individual request may increase slightly, but the overall system can handle more concurrent requests.
Conclusion
Spring Boot provides flexible mechanisms— Callable , WebAsyncTask , and DeferredResult —to implement asynchronous endpoints, each suited to different use cases. Proper thread‑pool configuration and careful selection of scenarios where the server thread can be released are essential to reap the benefits of higher throughput.
Top Architecture Tech Stack
Sharing Java and Python tech insights, with occasional practical development tool tips.
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.