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 execution flow, configuration of thread pools, and scenarios where async processing improves throughput.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Implementing Asynchronous Requests in Spring Boot: Callable, WebAsyncTask, and DeferredResult

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.

JavaSpring BootCallableasyncDeferredResultWebAsyncTask
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

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.