How do you make Spring application events asynchronous, and what changes about error handling and ordering?
answer
- @Async listener + @EnableAsync
- multicaster + setTaskExecutor => all async
- publish no longer blocks
- no ThreadLocal/security/tx propagation
- errors via setErrorHandler / AsyncUncaughtExceptionHandler
basics
~10 sAdd @Async to the @EventListener method (with @EnableAsync), or give the ApplicationEventMulticaster a TaskExecutor. Then listeners run on background threads, publishEvent no longer blocks, and listener exceptions no longer reach the publisher.
solid answer
~40 sTwo levers. Per-listener: annotate the @EventListener method with @Async and enable @EnableAsync — that listener runs on the async executor while others stay sync. Global: define an ApplicationEventMulticaster bean (SimpleApplicationEventMulticaster) with a TaskExecutor set, making all events async. Consequences: publishEvent returns immediately without waiting; each listener runs on a pool thread, so ThreadLocal/SecurityContext/transaction context does NOT propagate automatically; and exceptions thrown by an async listener are NOT propagated to the publisher — they go to the executor, and you handle them via SimpleApplicationEventMulticaster.setErrorHandler or an AsyncUncaughtExceptionHandler. Async also breaks the natural @Order guarantee across listeners. Use async for slow, fire-and-forget side effects; keep sync when the publisher needs the work done or its failure to matter.
code
java · 25 lines@Configuration
@EnableAsync
class EventAsyncConfig {
// Global route: makes ALL events async. Bean name is significant.
@Bean(name = "applicationEventMulticaster")
ApplicationEventMulticaster applicationEventMulticaster(BeanFactory bf) {
var multicaster = new SimpleApplicationEventMulticaster(bf);
var pool = new ThreadPoolTaskExecutor();
pool.setCorePoolSize(4);
pool.initialize();
multicaster.setTaskExecutor(pool);
// Async listener exceptions are handled here, not by the publisher:
multicaster.setErrorHandler(ex -> log.error("event listener failed", ex));
return multicaster;
}
}
@Component
class Mailer {
// Per-listener route: only this listener runs off-thread.
@Async
@EventListener
void on(UserRegistered e) { /* send welcome email */ }
}go deeper
Know that events are sync by default and @Async can move a listener off-thread.
Explain @EnableAsync requirement and that publish stops blocking.
Cover context loss, exception isolation, and ordering weakening; choose sync vs async deliberately.
Discuss bounded pools, TaskDecorator for context propagation, and async + transactional listener interactions.
## Default: synchronous The context's `ApplicationEventMulticaster` (default implementation `SimpleApplicationEventMulticaster`) delivers events **synchronously** on the publisher's thread. `publishEvent` blocks until all listeners return, and a listener exception aborts the rest and bubbles to the publisher. ## Way 1 — per-listener @Async ```java @Configuration @EnableAsync class AsyncConfig {} @Component class Mailer { @Async @EventListener void on(UserRegistered e) { /* slow email, runs off-thread */ } } ``` `@EnableAsync` installs an AOP proxy; `@Async` on the listener makes that specific listener run on the async `TaskExecutor` (by default a `SimpleAsyncTaskExecutor` unless you provide one). Other listeners for the same event remain synchronous. This is the most common, surgical approach. ## Way 2 — an async multicaster (global) Define a bean **named exactly `applicationEventMulticaster`** so Spring uses it: ```java @Bean(name = "applicationEventMulticaster") ApplicationEventMulticaster applicationEventMulticaster() { var m = new SimpleApplicationEventMulticaster(); m.setTaskExecutor(new SimpleAsyncTaskExecutor()); // or a pooled executor m.setErrorHandler(TaskUtils.LOG_AND_SUPPRESS_ERROR_HANDLER); return m; } ``` Now **every** event is dispatched via the executor. This is coarser and usually less desirable than per-listener `@Async`. ## What changes once async 1. **Non-blocking publish.** `publishEvent` returns without waiting for listeners. 2. **Thread context is lost.** The listener runs on a pool thread, so `SecurityContextHolder`, request-scoped beans, `ThreadLocal`s, and the current transaction/`EntityManager` are **not** propagated by default. You must propagate what you need (e.g., a `TaskDecorator`, or capture data into the event payload). 3. **Exception isolation.** An async listener's exception does **not** reach the publisher. With a multicaster executor, handle it via `setErrorHandler`. With `@Async`, an `AsyncUncaughtExceptionHandler` (from `AsyncConfigurer`) handles void-returning methods; a returned `Future`/`CompletableFuture` carries the exception instead. Unhandled, it's just logged/lost. 4. **Ordering weakens.** `@Order` orders how listeners are *invoked*, but async means they *complete* concurrently, so you can no longer rely on one finishing before another. ## When to choose which - **Sync (default):** the publisher needs the side effect done, or a failure should abort the operation (e.g., within a use-case that must stay consistent). Simpler, transactional context intact. - **Async @Async:** slow, best-effort, fire-and-forget side effects (emails, cache warmups, metrics) that must not slow or break the main flow. ## Gotchas - `@Async` needs `@EnableAsync`; without it the annotation is silently ignored and the listener stays synchronous. - Prefer a real bounded thread pool (`ThreadPoolTaskExecutor`) over the default `SimpleAsyncTaskExecutor`, which creates a new thread per task. - Combining async with `@TransactionalEventListener` is subtle: the listener still binds to the commit phase, then hops threads — losing the persistence context; design payloads to be self-contained. - Testing async events requires awaiting completion (Awaitility) since publish returns early.
- If an @Async @EventListener throws, does the publisher see the exception?No. Async delivery decouples it — for a void listener an AsyncUncaughtExceptionHandler (or the multicaster's errorHandler) handles it; the publisher's publishEvent call already returned successfully.
- Why might @Async on a listener seem to do nothing?You forgot @EnableAsync, or the method is called on a non-proxied path; without the async infrastructure enabled, @Async is ignored and the listener runs synchronously.
- What happens to the SecurityContext or current transaction inside an async listener?They aren't propagated by default — the listener runs on a different thread. You must propagate context explicitly (e.g., a TaskDecorator) or carry needed data in the event payload.
saying these in an interview costs you the question
- Thinking async listener exceptions still abort the publisher
- Expecting SecurityContext/transaction to propagate to the async thread
- Using @Async without @EnableAsync and expecting async behavior
- Relying on @Order for completion order once async