skip to content

Async MVC (Callable / DeferredResult)

Returning Callable, DeferredResult or WebAsyncTask releases the servlet thread while the work completes elsewhere, with timeouts and an executor you should configure. Interviewers use it to see whether you can get concurrency benefits without leaving the servlet stack.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

5

What happens when a Spring MVC controller method returns a Callable<T> instead of a plain value, and why would you do it?

level: juniorimportance: must knowfreq 45%

answer

  1. Callable -> WebAsyncManager -> AsyncTaskExecutor
  2. container thread released, response not committed
  3. async dispatch re-runs return-value handling
  4. default = SimpleAsyncTaskExecutor (unbounded, bad)
  5. configureAsyncSupport to set real executor

basics

~10 s

Spring runs the Callable on a separate thread pool and frees the servlet (Tomcat) thread immediately, so it can handle other requests. When the Callable finishes, Spring resumes and writes the response.

solid answer

~40 s

Returning a Callable<T> switches the request into asynchronous processing. Spring's WebAsyncManager submits the Callable to a TaskExecutor (an AsyncTaskExecutor) and returns the servlet container thread to Tomcat's pool right away, without committing the response. The container thread is now free to serve other requests while your slow work runs on the executor thread. When the Callable returns a value, Spring performs an async dispatch back to the container, and the return value goes through the normal handler-return-value machinery (message converters, view resolution, @ResponseBody) exactly as if it had been returned synchronously. The client sees no difference — it just waits for the response. The point is to avoid tying up the limited container thread pool during long or I/O-bound work.

code

java · 13 lines
java
@RestController
class ReportController {

  @GetMapping("/report")
  public Callable<Report> report() {
    // Returns immediately; container thread is freed.
    // The lambda runs on Spring's AsyncTaskExecutor.
    return () -> {
      Thread.sleep(3000);            // simulate slow work
      return new Report("quarterly"); // becomes the response body
    };
  }
}

go deeper

for a junior

Know that returning Callable frees the servlet thread and the work runs elsewhere; the client sees a normal response.

for a middle

Explain WebAsyncManager, the async dispatch, and the default SimpleAsyncTaskExecutor gotcha plus how to configure a real one.

for a senior

Discuss that it decouples pools without removing blocking, and how return-value handling re-runs on async dispatch.

for a principal

Frame when Callable async is worth it vs plain sync vs WebFlux, and executor sizing/back-pressure implications.

## The problem it solves A servlet container like Tomcat serves each incoming HTTP request on a thread from a fixed-size pool (e.g. 200 threads). If a controller does slow work (a slow downstream HTTP call, a long computation), that container thread is blocked and unavailable to any other request for the whole duration. Under load the pool exhausts and new requests queue or are rejected. **Asynchronous request processing** lets you hand the slow work to a *different* thread pool and give the container thread back immediately, so the container stays responsive. ## What `Callable<T>` does, step by step `java.util.concurrent.Callable<T>` is just `call()` returning a `T`. When your `@RequestMapping`/`@GetMapping` method returns one: 1. Spring's `RequestMappingHandlerAdapter` sees the return type and, via `CallableMethodReturnValueHandler`, starts async processing. 2. `WebAsyncManager` (obtained internally through `WebAsyncUtils.getAsyncManager(request)`) submits your `Callable` to an `AsyncTaskExecutor`. 3. The original **servlet container thread is released back to Tomcat** — but the HTTP response is *not* committed; the connection stays open. This uses the Servlet 3.0 `request.startAsync()` mechanism under the hood. 4. Your `Callable.call()` runs on an executor thread. Whatever it returns becomes the *concurrent result*. 5. Spring issues an **async dispatch** (`DispatcherType.ASYNC`) — the container re-dispatches the request back into the `DispatcherServlet`, which now picks up the concurrent result and runs it through the *normal* return-value handling: `@ResponseBody` + `HttpMessageConverter`, or view resolution, exception handling via `@ExceptionHandler`, etc. So `return () -> slowThing();` behaves for the client exactly like `return slowThing();` — same status, same body — but without holding the container thread during `slowThing()`. ## The default executor gotcha If you don't configure anything, Spring uses a **`SimpleAsyncTaskExecutor`**, which creates a **brand-new thread for every task and does not pool or cap them**. That is fine for a demo but dangerous in production (unbounded thread creation). You should register a real, bounded executor: ```java @Configuration public class AsyncConfig implements WebMvcConfigurer { @Override public void configureAsyncSupport(AsyncSupportConfigurer c) { ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor(); ex.setCorePoolSize(8); ex.setMaxPoolSize(32); ex.setQueueCapacity(100); ex.setThreadNamePrefix("mvc-async-"); ex.initialize(); c.setTaskExecutor(ex); c.setDefaultTimeout(30_000); } } ``` ## Key nuance: it does not reduce total threads The work still consumes a thread — just from a *different* pool. Async MVC with `Callable` **decouples** the container pool from the work pool; it does not make blocking work non-blocking. The real thread-count win comes with `DeferredResult` when *no* thread waits during idle time (long polling). For genuinely non-blocking I/O you'd use WebFlux (a sibling topic). ## When to use it - Long-running or I/O-bound handlers where you want to protect the container thread pool. - You want a specific, bounded, named executor for a class of work. - You want per-request timeouts (`WebAsyncTask`) on that work. If the work is trivial and fast, plain synchronous returns are simpler and better.

  • Does returning a Callable make blocking work non-blocking?
    No. The blocking work still occupies a thread, just one from the async TaskExecutor instead of the container pool. It decouples the pools but doesn't change the blocking nature. True non-blocking I/O requires WebFlux.
  • What executor runs the Callable by default and why is that a problem?
    SimpleAsyncTaskExecutor, which spawns a new unbounded thread per task with no pooling or cap. Under load this can exhaust memory/threads, so you should register a bounded ThreadPoolTaskExecutor via WebMvcConfigurer.configureAsyncSupport.

saying these in an interview costs you the question

  • Claiming Callable makes the handler non-blocking / reactive
  • Thinking the same servlet thread waits for the Callable to finish
  • Assuming the default executor is a bounded thread pool
  • Believing the response is written twice or the method runs twice

context

open as a page

What is the difference between returning a Callable<T> and a DeferredResult<T> from a Spring MVC controller?

level: middleimportance: must knowfreq 40%

basics

~20 s

With Callable, Spring runs your code on its own thread pool. With DeferredResult, Spring runs nothing — you complete it later from any thread by calling setResult(...), which is ideal for external events or long polling.

open as a page

What does WebAsyncTask<T> add over a plain Callable<T>, and how do async request timeouts work in Spring MVC?

level: middleimportance: should knowfreq 25%

basics

~10 s

WebAsyncTask wraps a Callable so you can set a per-request timeout and pick a specific executor, plus timeout/error callbacks. Timeouts otherwise come from a global setting and produce a 503 by default.

open as a page

Explain the WebAsyncManager mechanism: how is the servlet thread released and re-attached, and what happens to thread-locals like the SecurityContext during async MVC?

level: seniorimportance: should knowfreq 30%

basics

~20 s

WebAsyncManager calls request.startAsync() to free the container thread, runs the work on an executor, then triggers an async dispatch to resume. Thread-locals (SecurityContext, request scope) don't automatically move to the executor thread — Spring Security propagates them for Callable via a special filter, but you must handle it yourself for DeferredResult.

open as a page

As an architect, when would you choose async Spring MVC (Callable/DeferredResult) over plain blocking MVC, and how does it differ from choosing WebFlux? How do you size and isolate the executor?

level: principalimportance: should knowfreq 30%

basics

~20 s

Use async MVC to protect the container thread pool for long or event-driven work, or for long polling with DeferredResult. It still uses blocking threads — it just moves them off the container pool. WebFlux is different: true non-blocking I/O with few threads, but the whole stack must be reactive. Size executors as bounded, isolated pools sized to the work.

open as a page