Backend Development 10 min read

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

This article explains how Spring Boot supports asynchronous request handling after Servlet 3.0, demonstrates three practical approaches—Callable, WebAsyncTask, and DeferredResult—with code examples, shows how to configure a custom thread pool, and discusses when asynchronous processing improves throughput.

Top Architecture Tech Stack
Top Architecture Tech Stack
Top Architecture 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 from start to finish; after Servlet 3.0 the container can release the thread and related resources, reducing load and increasing throughput.

In a Spring Boot application there are four ways to implement asynchronous endpoints, but this article focuses on three: Callable , WebAsyncTask , and DeferredResult .

Implementation based on Callable

The controller returns a java.util.concurrent.Callable wrapping any value, which marks the endpoint as asynchronous.

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

The processing flow is:

Spring MVC calls request.startAsync() and submits the Callable to an AsyncTaskExecutor for execution in a separate thread.

The original DispatcherServlet and all filters exit the servlet thread while the response remains open.

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

The request is dispatched again to handle the asynchronous return value.

By default Spring uses SimpleAsyncTaskExecutor , which creates a new thread for each task; in production you should provide a real AsyncTaskExecutor with a thread pool.

Implementation based on WebAsyncTask

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

@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 timeout configured on WebAsyncTask overrides any global timeout setting.

Implementation based on DeferredResult

DeferredResult works similarly to Callable but the actual result may be 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;
}

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

The processing steps are:

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

Spring MVC calls request.startAsync() and releases the servlet thread while keeping the response open.

Another thread later sets the result on the stored DeferredResult , causing Spring MVC to dispatch the request back to the container for completion.

When returning a DeferredResult you can also configure a timeout that takes precedence over the global setting.

Providing a Thread Pool for Asynchronous Requests

Asynchronous requests free the main servlet thread, so a dedicated thread pool should be supplied for the background work.

@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) {
        // 0 or negative means never timeout
        configurer.setDefaultTimeout(60001);
        configurer.setTaskExecutor(asyncTaskExecutor);
    }
}

When to Use Asynchronous Requests

Asynchronous requests improve throughput when the request spends a lot of time waiting (e.g., calling external services) because the servlet thread is released and can handle other requests. They are not beneficial for CPU‑bound work where the thread would be busy anyway.

Increasing the maximum worker thread count can also meet demand, but the real gain of async processing appears when the CPU is idle during I/O waits. Although async adds thread‑switch overhead, the impact on response time is usually negligible.

threadpoolasynchronousSpring BootCallableDeferredResultWebAsyncTask
Top Architecture Tech Stack
Written by

Top Architecture Tech Stack

Sharing Java and Python tech insights, with occasional practical development tool tips.

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.