Ch. 8 · Spring Boot

Spring @Async and Executor Configuration

Run methods asynchronously with @Async, configure a bounded executor, and handle exceptions and the proxy boundary.

~2 min readadvancedupdated Oct 5, 2026

@Async makes a method run on a separate thread from a thread pool, which is useful for fire-and-forget work. It relies on the same proxy mechanism as transactions, so internal calls bypass it, and the default executor is often unsuitable for production.

Before you start

You should be comfortable with threads, beans and proxies. This article covers async execution and its configuration.

Step-by-step walkthrough

Step 1: Configure a real executor

The default simple executor may create an unbounded or single-threaded pool, which is not production-ready. Define a ThreadPoolTaskExecutor with a bounded queue and pool size, and reference it from @Async, so concurrency and memory are controlled.

Step 2: Respect the proxy boundary

Like caching, @Async is applied by a proxy, so an internal this.method() call runs synchronously. The method must be public and called through the injected bean. An async method that returns void swallows exceptions unless you configure a handler.

Step 3: Handle exceptions and results

A void async method loses exceptions; provide an AsyncUncaughtExceptionHandler to log them, or return a CompletableFuture so the caller can observe failure. Never assume an async failure surfaces on its own.

Worked scenario

The executor is bounded and exceptions are handled.

@Bean
public Executor taskExecutor() {
  ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor();
  ex.setCorePoolSize(4);
  ex.setMaxPoolSize(8);
  ex.setQueueCapacity(100);       // bounded: overload is rejected, not queued forever
  ex.initialize();
  return ex;
}
java

Walk through the example

The bounded queue means that when the pool is saturated, submissions are rejected rather than accumulating until memory runs out, which surfaces overload. The pool size controls concurrency. With an AsyncUncaughtExceptionHandler, a thrown exception in a void async method is logged instead of lost.

Common mistake

Using the default executor with an unbounded queue, so overload hides until memory is exhausted. Another is calling an async method internally and expecting it to be async, or returning void and losing exceptions.

Verify the behavior

Submit more tasks than the pool allows and confirm the bounded behavior. Trigger an exception in an async method and confirm the handler logs it. Call the method internally and confirm it runs synchronously, showing the proxy boundary.

Interview exercise

Why can a void @Async method lose its exception?

Answer and reasoning

Because a void async method returns no future, so there is no handle for the caller to observe, and the exception dies on the worker thread. Spring can route it to an AsyncUncaughtExceptionHandler, but only if one is configured. Returning a CompletableFuture gives the caller a way to see the failure.

Continue learning

Compare proxying in Transaction proxies and executor sizing in Thread pools. Read the Spring async execution documentation and try the Spring Boot interview questions.

More in Spring Boot

read ✓Spring Boot · hard

Spring Boot Caching Abstraction

Cache method results with @Cacheable, choose keys and TTLs, and evict on writes without the self-invocation trap.

~2 min readread →
read ✓Spring Boot · hard

Spring Declarative HTTP Clients

Define outbound HTTP as an annotated interface with @HttpExchange, create the proxy, and configure timeouts and errors.

~2 min readread →
read ✓Spring Boot · mid

Spring Boot Problem Details for APIs

Return consistent RFC 7807 error responses, map exceptions centrally, and avoid leaking internals to clients.

~2 min readread →
esc