How do @EventListener invocations behave with respect to threading and the publisher's transaction, and how do you change that?
answer
- sync + same thread + same tx by default
- publishEvent blocks until listeners done
- @Async + @EnableAsync -> off-thread, out of tx
- @Order for sync ordering
- async listener exceptions don't propagate
basics
~20 sBy default a listener runs on the same thread, inside the publisher's transaction, and blocks the publisher until it finishes. Add @Async (with @EnableAsync) to run it on a separate thread, which also removes it from that transaction.
solid answer
~40 s`publishEvent` is synchronous: Spring invokes each `@EventListener` inline on the caller's thread, so an exception in a listener propagates back to the publisher and, if there's a transaction, can roll it back. The listener also participates in the same transaction. To decouple, annotate the listener with `@Async` (requires `@EnableAsync`) or configure the `ApplicationEventMulticaster` bean with a `TaskExecutor` — then listeners run on pool threads, exceptions no longer reach the publisher, and they no longer share its transaction. Ordering among synchronous listeners is controlled with `@Order`. If you specifically want the listener to run relative to transaction commit/rollback rather than inline, use `@TransactionalEventListener`, which defers execution to a transaction phase (AFTER_COMMIT by default) instead of firing immediately.
code
java · 14 lines@Configuration
@EnableAsync
class AsyncConfig {}
@Component
class Listeners {
@EventListener // synchronous: runs in publisher's thread + transaction
void handleInline(OrderPlaced e) { /* exception here can roll back publisher */ }
@Async // off-thread: separate thread, NOT in publisher's transaction
@EventListener
void handleAsync(OrderPlaced e) { /* publisher returns without waiting */ }
}go deeper
Just knowing it's synchronous by default is enough.
Explain thread + transaction sharing and how @Async changes both.
Distinguish immediate synchronous dispatch from phase-based @TransactionalEventListener and the REQUIRES_NEW pitfall.
Reason about failure semantics: exception propagation vs async isolation and how that shapes reliability design.
## Default behavior of @EventListener When a bean calls `applicationEventPublisher.publishEvent(x)`, Spring's `SimpleApplicationEventMulticaster` looks up all matching listeners and **calls them one by one on the current thread**. Consequences: - **Blocking:** `publishEvent` does not return until every listener has completed. Slow listener = slow publisher. - **Same transaction:** if the publisher runs inside an `@Transactional` method, the listener executes within that same transaction and connection. A DB write in the listener commits/rolls back with the publisher's transaction. - **Exception propagation:** if a synchronous listener throws, the exception bubbles up out of `publishEvent` into the publisher. In a transaction that typically marks it rollback-only. (An `@Async` listener's exception does **not** propagate — it goes to the async exception handler / the returned `Future`.) - **Ordering:** multiple listeners for the same event fire in `@Order` order (lower value first); otherwise order is undefined. ## Making it asynchronous Two ways: 1. **`@Async` on the listener method** + `@EnableAsync` on a config class. The multicaster hands the invocation to the `@Async` executor, so the listener runs on a pool thread. It is now **outside** the publisher's transaction and thread; the publisher returns immediately. 2. **Configure the multicaster** — define a bean named `applicationEventMulticaster` of type `SimpleApplicationEventMulticaster` and set a `TaskExecutor`. Then **all** events are dispatched asynchronously. This is coarser than per-listener `@Async`. ### Implications of going async - You lose transaction sharing — the async listener sees committed state only if you sequence it after commit. - You lose exception propagation to the caller. - The event handling can outlive the request. ## Relationship to @TransactionalEventListener `@TransactionalEventListener` is still a synchronous listener by default, but instead of running **immediately** at `publishEvent`, Spring registers a `TransactionSynchronization` and runs it at a chosen **phase**: `BEFORE_COMMIT`, `AFTER_COMMIT` (default), `AFTER_ROLLBACK`, `AFTER_COMPLETION`. This lets you react to the outcome of the transaction rather than the mere act of publishing. Combine with `@Async` to also move it off-thread after commit. ## Gotchas - Assuming async by default — a common production surprise where a heavy listener stalls the request thread. - Doing DB writes in an `AFTER_COMMIT` listener: the original transaction is already committed, so those writes need a **new** transaction (`@Transactional(propagation = REQUIRES_NEW)`), otherwise they may silently not commit. - `@Async` self-invocation and proxy rules apply — the listener must be a Spring-managed bean.
- You add @Async to a listener that writes to the DB. What must you check about transactions?The listener no longer shares the publisher's transaction, so it needs its own @Transactional boundary to have a transaction at all, and it will only see data the publisher committed if it runs after commit.
- Does an exception in an @Async @EventListener roll back the publisher's transaction?No. It runs on another thread outside that transaction, so the exception is handled by the async infrastructure (AsyncUncaughtExceptionHandler / Future) and cannot affect the publisher.
saying these in an interview costs you the question
- Saying listeners run on a separate thread by default
- Assuming an async listener can roll back the publisher
- Writing to the DB in an AFTER_COMMIT listener without REQUIRES_NEW
- Thinking @Order controls async execution order