Mastering Asynchronous Requests in Spring Boot: Callable, WebAsyncTask & DeferredResult
This article explains how Spring Boot supports asynchronous request handling using Servlet 3.0 features, detailing four implementation approaches—Callable, WebAsyncTask, DeferredResult, and thread‑pool configuration—while outlining when async processing improves throughput and how to apply it effectively.
Preface
Servlet 3.0 before: each HTTP request is processed by a thread from start to finish.
Servlet 3.0 after: provides asynchronous request handling, releasing the thread and resources to increase throughput.
In Spring Boot there are four ways to implement async interfaces (excluding ResponseBodyEmitter, SseEmitter, StreamingResponseBody):
AsyncContext
Callable
WebAsyncTask
DeferredResult
The first, AsyncContext, is low‑level and rarely used, so the article focuses on the latter three.
Note: Server‑side async is invisible to the client; the response format does not change, and async may slightly increase response time for a single request.
Implementation based on Callable
In a controller, returning a java.util.concurrent.Callable indicates an async endpoint.
@GetMapping("/testCallAble")
public Callable<String> testCallAble() {
return () -> {
Thread.sleep(40000);
return "hello";
};
}The async processing steps are:
Controller returns a Callable.
Spring MVC calls request.startAsync() and submits the Callable to an AsyncTaskExecutor for execution in a separate thread.
DispatcherServlet and filters exit the servlet thread while keeping the response open.
When Callable produces a result, Spring MVC dispatches the request back to the servlet container to complete processing.
DispatcherServlet is invoked again to handle the async‑generated return value.
By default Callable uses SimpleAsyncTaskExecutor, which does not reuse threads; in practice you should configure a proper AsyncTaskExecutor.
Implementation based on WebAsyncTask
Spring’s WebAsyncTask wraps a Callable and adds callbacks for timeout, error, and completion.
@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;
}WebAsyncTask can configure a timeout that overrides the global setting.
Implementation based on DeferredResult
DeferredResultworks similarly to Callable but the actual result is set later from another thread.
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 completes. DeferredResult also supports custom timeout settings with higher priority than global configuration.
Providing a Thread Pool
Configure a dedicated thread pool for async tasks:
@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);
}
}When to Use Asynchronous Requests
Async requests improve throughput when the request spends a lot of time waiting (e.g., calling external services) so the servlet thread can be released. They are not beneficial for CPU‑bound tasks that keep the thread busy, and they add a small overhead due to thread switching.
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.
