Mastering Asynchronous Requests in Spring Boot: Callable, WebAsyncTask & DeferredResult
This article explains how Spring Boot leverages Servlet 3.0 asynchronous processing with four approaches—AsyncContext, Callable, WebAsyncTask, and DeferredResult—detailing their implementation, thread‑pool configuration, and when async endpoints truly boost throughput.
Preface
Before Servlet 3.0 each HTTP request was handled by a single thread from start to finish.
Since Servlet 3.0 asynchronous processing can release the container‑allocated thread and related resources, reducing system load and increasing service throughput.
Spring Boot offers four ways to implement asynchronous endpoints (excluding ResponseBodyEmitter, SseEmitter, and StreamingResponseBody): AsyncContext, Callable, WebAsyncTask, and DeferredResult.
Special note: asynchronous handling on the server side is invisible to the client; the response type does not change, and the latency increase is usually negligible.
Implementation Based on Callable
The controller returns a java.util.concurrent.Callable which marks the endpoint as asynchronous.
Controller returns a Callable.
Spring MVC invokes request.startAsync() and submits the Callable to an AsyncTaskExecutor for execution in a separate thread.
The DispatcherServlet and all filters exit the servlet thread while keeping the response open.
When the Callable produces a result, Spring MVC dispatches the request back to the servlet container to complete processing.
The DispatcherServlet processes the asynchronous result.
By default, Callable uses SimpleAsyncTaskExecutor, which does not reuse threads; in production you should configure a real AsyncTaskExecutor.
Implementation Based on WebAsyncTask
WebAsyncTaskis a wrapper around Callable that adds powerful callbacks such as timeout, error, and completion handling.
@GetMapping("/webAsyncTask")
public WebAsyncTask<String> webAsyncTask() {
WebAsyncTask<String> result = new WebAsyncTask<>(30003, () -> "success");
result.onTimeout(() -> "timeout callback");
result.onCompletion(() -> log.info("finish callback"));
return result;
} WebAsyncTaskcan configure its own timeout, which overrides the global timeout configuration.
Implementation Based on DeferredResult
DeferredResultworks similarly to Callable but the actual result is set later from another thread.
// Global map to store DeferredResult objects
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 client calls the first endpoint, the request stays in a pending state until another thread sets the result on the stored DeferredResult. It is essential to clean up expired or processed results (e.g., using DeferredResult.isSetOrExpired()) to avoid memory leaks.
deferredResult.onTimeout(() -> "timeout callback"); DeferredResultalso supports a custom timeout that takes precedence over the global timeout.
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;
}Register the custom executor in MVC async support 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 when the request spends most of its time waiting for external resources (e.g., calling other services). They are not beneficial for CPU‑bound work where the thread would be busy the whole time; in such cases simply increasing the worker thread pool is sufficient.
Because async processing introduces additional thread switches, the request latency may increase slightly, but the overhead is usually minimal compared with the gain in concurrency.
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.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn 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.
