At a high level, what is @TransactionalEventListener and why would you use it instead of @EventListener?
answer
- @EventListener bound to tx outcome
- default AFTER_COMMIT
- no tx => skipped unless fallbackExecution=true
- post-commit DB writes need REQUIRES_NEW
- use for external/irreversible side effects
basics
~20 s@TransactionalEventListener is a specialized @EventListener that runs a listener tied to a transaction's outcome — by default only after the transaction commits — so side effects happen only when the data actually persisted. A plain @EventListener runs immediately, before commit.
solid answer
~40 s@TransactionalEventListener is an @EventListener variant whose invocation is bound to the surrounding transaction's lifecycle instead of firing immediately on publish. The default phase is AFTER_COMMIT, meaning the listener runs only once the publisher's transaction has successfully committed — so you don't email a user or call another system for a change that later rolled back. Other phases exist (before commit, after rollback, after completion) for completeness. Key overview points: if there is no active transaction when the event is published, the listener is skipped by default unless fallbackExecution=true; and because AFTER_COMMIT runs after the transaction, new database writes there need a fresh transaction. It solves the classic 'published an event, then the transaction rolled back' consistency bug that plain synchronous @EventListener has. (Exact phase-binding mechanics are a transaction-management topic.)
code
java · 22 lines@Service
class RegistrationService {
private final ApplicationEventPublisher publisher;
RegistrationService(ApplicationEventPublisher publisher) { this.publisher = publisher; }
@Transactional
void register(String email) {
// ...persist user in this transaction...
publisher.publishEvent(new UserRegistered(email));
// If this method throws after publishing, the transactional
// listener below will NOT run (transaction rolls back).
}
}
@Component
class WelcomeHandler {
// Runs only AFTER the register() transaction commits successfully
@TransactionalEventListener
void on(UserRegistered e) {
emailService.sendWelcome(e.email()); // safe irreversible side effect
}
}go deeper
Know it delays the listener until after the transaction commits.
Contrast with @EventListener firing immediately before commit and name the rollback-consistency problem it solves.
Cover the no-transaction skip, fallbackExecution, and REQUIRES_NEW for post-commit writes.
Discuss durability limits (crash after commit) and reaching for an outbox / Modulith event publication registry for guaranteed delivery.
## The problem it solves With a plain `@EventListener`, delivery is **synchronous and immediate**: the listener runs the instant `publishEvent` is called — which is usually **inside** the publisher's still-open transaction. If that transaction later **rolls back**, you've already run the side effect (sent an email, called a payment API, published a message). The database change vanished but the side effect happened — an inconsistency. ## What @TransactionalEventListener does `org.springframework.transaction.event.TransactionalEventListener` is a specialization of `@EventListener` that **defers** the listener until a chosen point in the surrounding transaction's lifecycle rather than firing on publish. By **default** it runs in the **AFTER_COMMIT** phase — i.e., only after the transaction that was active when the event was published **commits successfully**. So the side effect fires exactly when the data is durably persisted. Conceptually it supports several transaction phases (before commit, after commit, after rollback, after completion) so you can also react to failures; the precise semantics and ordering of those phases are a transaction-management concern (owned elsewhere) — for this topic, the headline is *AFTER_COMMIT by default, bound to the transaction outcome*. ## Two must-know overview gotchas 1. **No active transaction ⇒ listener is skipped.** If the event is published when **no** transaction is running, a `@TransactionalEventListener` does **nothing** by default. To make it run anyway (like a normal listener) set `fallbackExecution = true`. This bites people in tests or non-transactional call paths. 2. **Writes after commit need a new transaction.** Because AFTER_COMMIT runs *after* the original transaction finished, the persistence context may be closed; any DB write you do there must open a **new** transaction (e.g., a method marked `REQUIRES_NEW`). Otherwise updates silently don't persist. ## Usage shape ```java @Component class NotificationHandler { @TransactionalEventListener // default phase = AFTER_COMMIT void on(UserRegistered e) { // safe: the user really was committed emailService.sendWelcome(e.email()); } } ``` The publisher just calls `publishEvent(new UserRegistered(...))` inside its `@Transactional` method as usual. `@TransactionalEventListener` also accepts a `condition` SpEL like `@EventListener`. ## When to use which - **@EventListener:** the reaction is part of the same unit of work, should share the transaction, and should roll back with it (e.g., updating another aggregate in the same DB). Runs before commit. - **@TransactionalEventListener:** the reaction is an **external/irreversible side effect** (email, message broker, third-party call) that must only happen if the transaction actually committed. ## Gotchas recap - Skipped silently without a transaction (unless `fallbackExecution=true`). - AFTER_COMMIT DB writes need `REQUIRES_NEW`. - Making such a listener `@Async` moves it off-thread after commit, losing the persistence context — design self-contained payloads. - It's still in-process and unpersisted: if the JVM crashes between commit and the AFTER_COMMIT listener, the side effect is lost (for guaranteed delivery consider an outbox pattern / Spring Modulith's event publication registry).
- What happens if a @TransactionalEventListener event is published with no active transaction?By default the listener is skipped entirely. Set fallbackExecution=true to have it run like a normal @EventListener when no transaction is present.
- Why can a database write inside a default (AFTER_COMMIT) transactional listener silently fail to persist?It runs after the original transaction committed, so there's no active transaction/persistence context; you must open a new one (e.g., @Transactional(propagation = REQUIRES_NEW)) for writes to commit.
- Which side effects specifically justify @TransactionalEventListener over @EventListener?External or irreversible ones — emails, calls to third-party APIs, publishing to a message broker — that must only occur if the data actually committed, not on a change that later rolls back.
saying these in an interview costs you the question
- Believing @TransactionalEventListener runs even without a transaction by default
- Doing DB writes in an AFTER_COMMIT listener without REQUIRES_NEW
- Using it for work that should roll back with the publisher
- Assuming it guarantees delivery across a JVM crash