skip to content

Explain @TransactionalEventListener: what problem it solves, its phases, and why it's the common bridge from application events to a broker.

level: seniorimportance: must knowfreq 50%

answer

  1. defers listener to a transaction phase
  2. AFTER_COMMIT default = publish only committed facts
  3. phases: BEFORE_COMMIT / AFTER_COMMIT / AFTER_ROLLBACK / AFTER_COMPLETION
  4. fallbackExecution=false -> skipped without a tx
  5. AFTER_COMMIT writes need REQUIRES_NEW; dual-write -> outbox

basics

~20 s

@TransactionalEventListener delays a listener until the publisher's transaction reaches a chosen phase — usually AFTER_COMMIT. That way you only act (e.g. send a broker message) on data that actually committed, avoiding notifications about work that later rolled back.

solid answer

~50 s

A plain `@EventListener` runs the moment `publishEvent` is called, before the transaction commits, so if you send a Kafka/RabbitMQ message there and the transaction later rolls back, you've announced a change that never happened. `@TransactionalEventListener` binds the listener to a `TransactionSynchronization` and defers it to a phase: `AFTER_COMMIT` (default), `BEFORE_COMMIT`, `AFTER_ROLLBACK`, or `AFTER_COMPLETION`. AFTER_COMMIT guarantees the DB change is durable before you publish outward, making it the natural bridge: producer publishes an in-JVM event, an AFTER_COMMIT listener converts it into a broker message. By default it only runs if there is a transaction (`fallbackExecution=false`). Caveat: at AFTER_COMMIT the transaction is closed, so any DB writes in the listener need `REQUIRES_NEW`, and the outward publish is a second write that can still fail after commit (dual-write) — which is why durable systems add a transactional outbox.

code

java · 15 lines
java
@Component
class OrderEventRelay {

    private final StreamBridge streamBridge; // Spring Cloud Stream -> broker
    OrderEventRelay(StreamBridge streamBridge) { this.streamBridge = streamBridge; }

    // Runs only after the producing transaction commits successfully.
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    void publishToBroker(OrderPlaced event) {
        // Safe: the order row is durably committed before we announce it.
        streamBridge.send("orders-out-0", event);
        // Caveat: if this send fails after commit, the DB is updated but no
        // message went out (dual-write). A transactional outbox closes that gap.
    }
}

go deeper

for a junior

Beyond scope; just know something ties listeners to commit.

for a middle

Know AFTER_COMMIT exists and why you'd publish there.

for a senior

Explain the phases, fallbackExecution, and the bridge-to-broker pattern fluently.

for a principal

Drive to the dual-write limitation and prescribe the transactional outbox as the durable solution.

## The problem With a normal `@EventListener`, the listener fires **synchronously at `publishEvent`**, which is still **inside** the producer's open transaction. If the listener triggers a side effect that escapes the transaction — sending a message to a broker, calling another service, sending an email — and the transaction **later rolls back**, that side effect has already happened. You've told the world about a state change that was undone. This is the classic ‘published before commit' bug. ## What @TransactionalEventListener does It makes the listener **transaction-aware**. Instead of invoking immediately, Spring registers a `TransactionSynchronization` on the current transaction and calls the listener at a chosen **phase**: - **`AFTER_COMMIT`** (default) — after a successful commit. Safe place to publish outward effects. - **`BEFORE_COMMIT`** — just before commit flush; runs while the transaction can still be affected. - **`AFTER_ROLLBACK`** — after a rollback (e.g. compensating/logging). - **`AFTER_COMPLETION`** — after commit or rollback (inspect status). ### fallbackExecution By default, if there is **no active transaction**, the listener is **skipped** (`fallbackExecution=false`). Set `fallbackExecution=true` to run it immediately (as a normal listener) when there's no transaction — important in tests or non-transactional paths. ## Why it's the bridge to messaging The idiomatic decoupling pattern: 1. Domain/service code does its DB work in a transaction and publishes a lightweight **in-JVM** application event (`OrderPlaced`). 2. A `@TransactionalEventListener(phase = AFTER_COMMIT)` receives it **only after the row is durably committed** and translates it into a **broker** message (`StreamBridge.send(...)`, `KafkaTemplate.send(...)`, `SimpMessagingTemplate`, etc.). This keeps the producer ignorant of the broker (testable, decoupled) while ensuring outward messages describe committed facts. ## Critical gotchas - **DB writes in AFTER_COMMIT need a new transaction.** The original transaction is already committed and closed; a write without `@Transactional(propagation = REQUIRES_NEW)` may have no transaction or silently not persist. - **Dual-write / non-atomicity.** DB commit and broker publish are two separate resources. If the app crashes between commit and publish (or the broker is down), the DB changed but no message was sent — lost event. AFTER_COMMIT does **not** make them atomic. The robust fix is the **transactional outbox** (write the event to an outbox table in the same transaction, a relay ships it to the broker). - **Async option.** Combine with `@Async` to move the post-commit work off the request thread. - **Ordering/self-invocation** rules for proxies still apply. ## When NOT to use it If the effect must be part of the same atomic unit (e.g. must roll back together), don't push it after commit — keep it in-transaction or use an outbox. If there's no transaction at all, a plain `@EventListener` is simpler.

  • Why can an AFTER_COMMIT listener fail to persist its own DB writes?
    Because the original transaction is already committed and closed when it runs; without propagation = REQUIRES_NEW there is no active transaction to commit those writes, so they may be discarded.
  • Does AFTER_COMMIT make the DB commit and the Kafka send atomic?
    No. They are two independent resources. A crash or broker outage between commit and send loses the message. Achieving atomicity requires a transactional outbox (or an XA/2PC setup, rarely used).
  • What happens if you publish the event from a non-transactional method?
    By default (fallbackExecution=false) the @TransactionalEventListener is skipped entirely. Set fallbackExecution=true to have it execute immediately like a normal listener.

saying these in an interview costs you the question

  • Claiming AFTER_COMMIT makes DB + broker atomic
  • Sending broker messages from a plain @EventListener before commit
  • Doing DB writes in AFTER_COMMIT without REQUIRES_NEW
  • Not knowing the listener is skipped without a transaction by default

context