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 execution flow, configuration of thread pools, and scenarios where async processing improves throughput.
Preface
Before Servlet 3.0 each HTTP request was processed by a single thread from start to finish. Servlet 3.0 introduced asynchronous request processing, allowing the container thread to be released early, reducing load and increasing throughput.
In a Spring Boot application there are four ways to implement asynchronous endpoints (ResponseBodyEmitter, SseEmitter, StreamingResponseBody are omitted here): AsyncContext, Callable, WebAsyncTask, and DeferredResult.
Note: Asynchronous processing on the server side is invisible to the client; the response format does not change, and the latency increase for a single request is usually negligible.
Implementation Based on Callable
Returning a java.util.concurrent.Callable from a controller method marks the endpoint as asynchronous.
@GetMapping("/testCallAble")
public Callable
testCallAble() {
return () -> {
Thread.sleep(40000);
return "hello";
};
}The controller returns a Callable, Spring MVC calls request.startAsync() and submits the Callable to an AsyncTaskExecutor . The original servlet thread and filters exit, keeping the response open. When the Callable completes, Spring MVC dispatches the request back to the servlet container to finish processing.
By default Spring uses SimpleAsyncTaskExecutor , which creates a new thread for each task; in production a real AsyncTaskExecutor should be configured.
Implementation Based on WebAsyncTask
WebAsyncTask is a wrapper around Callable that adds callbacks for timeout, error, and completion. It is usually preferred over raw Callable.
@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 configured on WebAsyncTask overrides any global timeout settings.
Implementation Based on DeferredResult
DeferredResult works similarly to Callable but the actual result is 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;
}When the client calls this endpoint the request stays in a pending state until another thread sets 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";
}DeferredResult also supports a custom timeout that takes precedence over global settings. In practice you should clean up expired or processed DeferredResult objects to avoid memory leaks.
Remember to periodically remove invalid DeferredResult instances (e.g., using DeferredResult.isSetOrExpired() ) from the storage.
Providing a Thread Pool
Asynchronous requests should use a dedicated thread pool instead of the default executor.
@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;
}Configure Spring MVC to use this 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);
}
}When to Use Asynchronous Requests
Async requests improve throughput when the request spends a lot of time waiting (e.g., calling external services) because the servlet thread can be released. They are not beneficial for CPU‑bound work that keeps the thread busy.
Increasing the worker thread pool size can also meet demand, but async processing is advantageous when the CPU is idle during I/O waits.
Although async processing adds thread‑switch overhead, the additional latency is usually minimal.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.