Backend Development 9 min read

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

This article explains how Spring Boot supports asynchronous request processing using Servlet 3.0 features and four approaches—AsyncContext, Callable, WebAsyncTask, and DeferredResult—provides code examples, thread‑pool configuration, and guidance on when to apply async handling to improve throughput.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Implementing Asynchronous Requests in Spring Boot: Callable, WebAsyncTask, and DeferredResult

Introduction – Before Servlet 3.0 each HTTP request was handled 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 a Spring Boot application there are four ways to implement async endpoints: ResponseBodyEmitter , SseEmitter , StreamingResponseBody (not covered here), and the three methods described below: AsyncContext (native servlet, rarely used), Callable , WebAsyncTask , and DeferredResult .

Using Callable – Returning a java.util.concurrent.Callable from a controller method marks the endpoint as asynchronous. Spring MVC starts async processing, submits the Callable to an AsyncTaskExecutor , and releases the servlet thread while the result is computed in another thread.

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

The client still receives a plain String response, identical to a synchronous method.

Using 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 set on the WebAsyncTask overrides any global timeout configuration.

Using DeferredResult – DeferredResult allows the controller to return immediately while another thread later sets the actual result.

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

When another thread calls deferredResult.setResult(...) , the pending request completes with that value. It is also possible to configure a per‑request timeout that takes precedence over the global setting.

Providing a Thread Pool – Asynchronous handling 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-");
    asyncServiceExecutor.setBeanName("TaskId" + taskId);
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
    executor.setWaitForTasksCompleteOnShutdown(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 – Async processing improves throughput only when the request spends a lot of time waiting (e.g., calling external services). If the request is CPU‑bound, moving work to another thread does not help and may even add overhead.

Conclusion – The article ends with a promotional note encouraging readers to follow the author’s public account for PDF collections of Spring Cloud, Spring Boot, and MyBatis advanced tutorials.

JavaasynchronousSpring BootCallableDeferredResultWebAsyncTask
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

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.