Backend Development 9 min read

Implementing Asynchronous Requests in Spring Boot: Callable, WebAsyncTask, and DeferredResult

This article explains how to use Spring Boot's asynchronous request handling with Callable, WebAsyncTask, and DeferredResult, compares their processing flows, shows how to configure a custom thread pool, and discusses when asynchronous endpoints improve throughput in backend services.

Architect's Tech Stack
Architect's Tech Stack
Architect's Tech Stack
Implementing Asynchronous Requests in Spring Boot: Callable, WebAsyncTask, and DeferredResult

Before Servlet 3.0 each HTTP request was processed by a single thread; after Servlet 3.0 asynchronous processing allows releasing the thread to improve throughput.

Spring Boot provides four ways to implement async endpoints; this article focuses on Callable, WebAsyncTask, and DeferredResult.

Implementation based on Callable

Returning a java.util.concurrent.Callable from a controller method makes the endpoint asynchronous. Example code:

@GetMapping("/testCallAble")
public Callable
testCallAble(){
    return () -> {
        Thread.sleep(40000);
        return "hello";
    };
}

The server-side async processing is invisible to the client; the final response is a simple String just like a synchronous method. The processing steps are:

Spring MVC calls request.startAsync() and submits the Callable to an AsyncTaskExecutor .

The original servlet thread and filters exit, but the response remains open.

When the Callable produces a result, Spring MVC dispatches the request back to the servlet container.

The DispatcherServlet processes the asynchronous return value.

Callable uses SimpleAsyncTaskExecutor by default, which does not reuse threads; a custom AsyncTaskExecutor should be configured for production.

Implementation based on WebAsyncTask

WebAsyncTask is a wrapper around Callable that adds timeout, error, and completion callbacks. Example code:

@GetMapping("/webAsyncTask")
public WebAsyncTask
webAsyncTask(){
    WebAsyncTask
result = new WebAsyncTask<>(30003, () -> {
        return "success";
    });
    result.onTimeout(() -> {
        log.info("timeout callback");
        return "timeout callback";
    });
    result.onCompletion(() -> log.info("finish callback"));
    return result;
}

The configured timeout (30003 ms) overrides any global timeout settings.

Implementation based on DeferredResult

DeferredResult works similarly to Callable but the actual result can be set later from another thread. Example of storing a DeferredResult in a map:

// Define a 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;
}

Another endpoint sets the result:

@GetMapping("/testSetDeferredResult")
public String testSetDeferredResult() throws InterruptedException {
    DeferredResult
deferredResult = deferredResultMap.get("test");
    boolean flag = deferredResult.setResult("testSetDeferredResult");
    if(!flag){
        log.info("结果已经被处理,此次操作无效");
    }
    return "ok";
}

Processing flow for DeferredResult:

The controller returns a DeferredResult and stores it in a shared collection.

Spring MVC calls request.startAsync() and releases the servlet thread.

Another thread later sets the result via deferredResult.setResult(...) .

Spring MVC dispatches the request back to the servlet container to complete the response.

Providing a Custom Thread Pool

Define a ThreadPoolTaskExecutor bean to be used 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;
}

Register the executor in MVC configuration:

@Configuration
public class FykWebMvcConfigurer implements WebMvcConfigurer {
    @Autowired
    @Qualifier("mvcAsyncTaskExecutor")
    private AsyncTaskExecutor asyncTaskExecutor;

    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        // 0 or negative means never timeout
        configurer.setDefaultTimeout(60001);
        configurer.setTaskExecutor(asyncTaskExecutor);
    }
}

When to Use Asynchronous Requests

Async requests improve throughput when the request spends most of its time waiting (e.g., calling external services) rather than performing CPU‑bound computation; they free Tomcat worker threads for other requests while adding only minimal latency.

ThreadPoolSpring BootCallableAsyncDeferredResultWebAsyncTask
Architect's Tech Stack
Written by

Architect's Tech Stack

Java backend, microservices, distributed systems, containerized programming, and more.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.