Backend Development 10 min read

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 apply asynchronous processing to improve throughput.

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

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 load and increasing throughput.

In Spring Boot there are four ways to create asynchronous endpoints (excluding ResponseBodyEmitter, SseEmitter, and StreamingResponseBody which are not covered here):

AsyncContext

Callable

WebAsyncTask

DeferredResult

Callable implementation

Returning a java.util.concurrent.Callable from a controller method marks the endpoint as asynchronous. The framework submits the Callable to an AsyncTaskExecutor and releases the servlet thread while the Callable runs in a separate thread.

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

The processing steps are:

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

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

When the Callable finishes, Spring MVC dispatches the request back to the container to complete the response.

The DispatcherServlet then uses the Callable’s result to continue processing.

By default Spring uses SimpleAsyncTaskExecutor , which creates a new thread for each task. In production you should provide a real AsyncTaskExecutor implementation.

WebAsyncTask implementation

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

@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 the WebAsyncTask overrides any global timeout settings.

DeferredResult implementation

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;
}

Clients calling this endpoint will see the request stay 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";
}

When the result is set, the pending request completes and the client receives the value. DeferredResult also supports custom timeout values that take precedence over global settings. It is important to clean up expired or completed DeferredResult objects to avoid memory leaks.

Providing a custom thread pool

Asynchronous requests should use a dedicated thread pool rather than the default executor. Example configuration:

@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 with Spring MVC:

@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 processing improves throughput when the request spends a lot of time waiting (e.g., calling external services) because the servlet thread can be released to handle other requests. It is not beneficial for CPU‑bound work that continuously occupies the thread, as the work would simply be moved to another thread without reducing overall load. Increasing the maximum worker thread count can also address some scenarios, but async is preferable when the thread would otherwise be idle.

Although async adds a small overhead due to thread switching, the impact on latency is minimal compared with the gains in concurrency.

threadpoolasynchronousSpring BootCallableDeferredResultWebAsyncTask
Architecture Digest
Written by

Architecture Digest

Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.

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.