skip to content

@TransactionalEventListener Phases

@TransactionalEventListener binds an event to a phase — before commit, after commit by default, after rollback, or after completion — instead of firing immediately. Interviewers ask because it is the clean answer to 'how do you avoid publishing an event for data that never committed'.

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

questions

5

What is @TransactionalEventListener and what is its default phase?

level: juniorimportance: must knowfreq 55%

answer

  1. specialization of @EventListener
  2. default = AFTER_COMMIT
  3. ties handler to tx lifecycle
  4. no tx + no fallback = skipped
  5. registers TransactionSynchronization

basics

~10 s

It's a Spring event listener that only fires relative to a transaction. By default it runs in the AFTER_COMMIT phase, so it executes only after the surrounding transaction successfully commits.

solid answer

~30 s

@TransactionalEventListener is a specialization of @EventListener that binds the listener's execution to the lifecycle of the transaction that was active when the event was published. Instead of running synchronously at publish time (like @EventListener), it registers a transaction synchronization callback and runs at a chosen phase. The default phase is TransactionPhase.AFTER_COMMIT, meaning the handler runs only after the publishing transaction commits successfully. This is the common pattern for 'do side effects only if the data was actually persisted' — sending emails, publishing to a message broker, etc. If no transaction is active, by default the listener is silently skipped unless fallbackExecution=true.

code

java · 18 lines
java
@Component
public class OrderNotifier {

    // Default phase is AFTER_COMMIT: fires only after a successful commit
    @TransactionalEventListener
    public void onOrderPlaced(OrderPlacedEvent event) {
        // safe to send an email — the order row is durably committed
        emailService.sendConfirmation(event.orderId());
    }
}

// Publisher side (inside a @Transactional method)
@Transactional
public void placeOrder(Order order) {
    orderRepository.save(order);
    eventPublisher.publishEvent(new OrderPlacedEvent(order.getId()));
    // listener runs after this method's tx commits
}

go deeper

for a junior

Know it's @EventListener bound to a transaction and the default is AFTER_COMMIT.

for a middle

Explain the no-transaction skip behavior and fallbackExecution.

for a senior

Discuss the TransactionSynchronization mechanism and why AFTER_COMMIT is the safe default for side effects.

for a principal

Frame it as a durability boundary for outbox/side-effect patterns and reason about failure modes when no tx is present.

## What it is `@TransactionalEventListener` (package `org.springframework.transaction.event`) is a specialized form of `@EventListener`. A normal `@EventListener` handles an application event **synchronously at the moment `ApplicationEventPublisher.publishEvent(...)` is called** — regardless of any transaction. `@TransactionalEventListener` instead **defers** the handler and ties it to the **current transaction's lifecycle**, so it fires at a specific point relative to commit/rollback. ## How it works under the hood When an event is published inside an active transaction, Spring registers a `TransactionSynchronization` callback (via `TransactionSynchronizationManager`). That callback fires the listener during the transaction's commit/completion sequence, at the configured `TransactionPhase`. ## The `phase` attribute (TransactionPhase enum) - `BEFORE_COMMIT` — runs just before the transaction commits (during `beforeCommit`). Still inside the transaction, so DB writes here participate in the **same** commit. - `AFTER_COMMIT` — **the default**. Runs after a successful commit. The transaction is already committed; new DB work here needs a new transaction. - `AFTER_ROLLBACK` — runs after the transaction rolls back. - `AFTER_COMPLETION` — runs after the transaction completes, **regardless** of commit or rollback outcome. (`AFTER_COMMIT` and `AFTER_ROLLBACK` are mutually exclusive refinements of `AFTER_COMPLETION`.) ## fallbackExecution By default, if there is **no active transaction** when the event is published, the listener does **not** run at all (Spring logs at trace/debug that no transaction synchronization is active). Setting `fallbackExecution = true` makes the listener run immediately, like a plain `@EventListener`, when no transaction is present. ## When to use which phase - `AFTER_COMMIT` (default): trigger external side effects only if the data was durably persisted — send emails, enqueue messages, invalidate caches. Most common. - `BEFORE_COMMIT`: do additional work that must be part of the same atomic commit (extra validation, additional persistence that should roll back together). - `AFTER_ROLLBACK`: compensation/cleanup after failure. - `AFTER_COMPLETION`: unconditional cleanup regardless of outcome. ## Key gotchas - The default `AFTER_COMMIT` handler runs **after** commit, so any JPA/DB operations you perform there are **not** part of the committed transaction. If you write to the DB in an AFTER_COMMIT handler using the same (now read-only-ish) context, it may be silently lost unless you open a **new** transaction (e.g., `@Transactional(propagation = REQUIRES_NEW)`). - Without an active transaction and without `fallbackExecution=true`, your handler **silently does nothing** — a classic 'my listener never fires' bug. - The event must be published **while a transaction is active** for the deferral to work.

  • How does @TransactionalEventListener differ from @EventListener?
    @EventListener runs synchronously at publishEvent() time regardless of any transaction. @TransactionalEventListener defers execution and binds it to the current transaction's phase (default AFTER_COMMIT), and by default is skipped entirely if no transaction is active.
  • What happens if you publish the event outside a transaction?
    By default the listener is not invoked at all. You must set fallbackExecution=true for it to run immediately like a normal @EventListener.

saying these in an interview costs you the question

  • Thinking the default phase is BEFORE_COMMIT
  • Believing the listener always runs even without a transaction
  • Confusing it with @EventListener (synchronous at publish time)

context

open as a page

Explain the four TransactionPhase values (BEFORE_COMMIT, AFTER_COMMIT, AFTER_ROLLBACK, AFTER_COMPLETION) and when each fires.

level: middleimportance: must knowfreq 50%

basics

~10 s

BEFORE_COMMIT fires just before commit (still in the tx). AFTER_COMMIT (default) fires after a successful commit. AFTER_ROLLBACK fires after a rollback. AFTER_COMPLETION fires after the tx ends either way.

open as a page

What is the fallbackExecution attribute and when would you set it to true?

level: middleimportance: should knowfreq 35%

basics

~10 s

fallbackExecution controls what happens when no transaction is active. Default false means the listener is skipped. Setting it true makes the listener run immediately (like a plain @EventListener) even without a transaction.

open as a page

Why do database writes performed inside an AFTER_COMMIT listener often get silently lost, and how do you fix it?

level: seniorimportance: should knowfreq 45%

basics

~10 s

The original transaction is already committed when AFTER_COMMIT runs, so there's no active writable transaction. Persistence there isn't flushed/committed. Fix it by starting a new transaction with @Transactional(propagation = REQUIRES_NEW).

open as a page

How do @Async, exception handling, and durability interact with AFTER_COMMIT listeners, and where does the outbox pattern fit?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

By default the AFTER_COMMIT listener runs synchronously in the committing thread; adding @Async moves it to another thread. Exceptions after commit only get logged. If the process crashes after commit but before the side effect, the side effect is lost — the transactional outbox pattern closes that gap.

open as a page