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.

Java Architect Essentials
Java Architect Essentials
Java Architect Essentials
Mastering Asynchronous Requests in Spring Boot: Callable, WebAsyncTask & DeferredResult

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

DeferredResult

works 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.

backendThreadPoolSpringBootCallableAsyncDeferredResultWebAsyncTask
Java Architect Essentials
Written by

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.

0 followers
Reader feedback

How this landed with the community

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.