@EventListener, @TransactionalEventListener, ApplicationEventPublisher
- Last updated
- Prerequisites:
- Spring beans and DI
- Understanding of transactions and loose coupling
- spring
- events
- event-listener
- transactional-event-listener
- event-publisher
- async
Read at your depth
The practical view
Spring events decouple components: publish with ApplicationEventPublisher.publishEvent(new OrderPlacedEvent(order)); listen with @EventListener on a method: @EventListener public void on(OrderPlacedEvent e) { ... }. The listener runs synchronously by default in the publisher's thread. For async, add @Async to the listener (and @EnableAsync). @TransactionalEventListener(phase = AFTER_COMMIT) runs the listener only after the transaction commits — perfect for sending emails/notifications that should not happen if the transaction rolls back. @TransactionalEventListener has a fallbackExecution attribute to also run when there is no transaction.
The same idea in other frameworks
N/A — server-side Spring concern.N/A — Spring concern.N/A — Spring concern.N/A — Spring concern.Legacy vs modern
Direct service-to-service calls vs events
Calling another service directly couples the components and runs regardless of transaction outcome; events decouple them and can wait for commit.
public void placeOrder(Order o) {
orderRepo.save(o);
emailService.sendConfirmation(o); // ❌ OLD: coupled + sent before commit
}publisher.publishEvent(new OrderPlacedEvent(o)); // ✅ NEW: decoupled
@TransactionalEventListener(phase = AFTER_COMMIT) // ✅ NEW
public void on(OrderPlacedEvent e) { emailService.sendConfirmation(e.order()); }Interview gotchas
Context
This tests the transaction-aware event model — interviewers want the commit/rollback semantics that prevent acting on data that never materialized.
The mechanical answer
A plain @EventListener runs synchronously at publish time, which is INSIDE the transaction (if the publisher method is @Transactional). If the transaction later rolls back, the listener has already acted on data that no longer exists — an email sent for an order that was never committed. @TransactionalEventListener with phase AFTER_COMMIT registers the listener to be invoked by the transaction synchronization AFTER the commit succeeds: the listener runs only when the data is durable, and it does NOT run at all if the transaction rolls back (unless you also use AFTER_ROLLBACK or fallbackExecution). This guarantees side effects are consistent with committed state. The trade-off: the listener runs later (after commit), so it can't affect the transaction, and the commit must succeed for it to fire.
Trap
The naive answer is '@TransactionalEventListener just delays the listener'. It binds the listener to the transaction outcome — no commit, no callback (for AFTER_COMMIT). Another trap: assuming the listener shares the transaction — after commit, the transaction is gone, so the listener starts its own (or none); data read in the listener sees the committed state. Candidates who explain the synchronization binding show real Spring event mastery.
Context
This probes the failure semantics — interviewers want to know whether an exception in a listener affects the publisher/transaction and what async execution changes.
The mechanical answer
With the default synchronous multicaster, a listener exception propagates back to the publisher (publishEvent) — so if a @EventListener throws, the exception bubbles into the publishing method and can roll back its transaction (if @Transactional). This is often surprising: a failing notification listener can break the core operation. With @Async listeners, the invocation happens on the async executor thread, so the exception does NOT propagate to the publisher — the core flow is isolated, but the failure is silent unless you handle it in the listener or a global async exception handler (AsyncUncaughtExceptionHandler). @TransactionalEventListener exceptions: after commit, an exception propagates from the afterCommit callback, which can cause an error at the transaction boundary. Best practice: make listeners resilient (catch and log, or retry) so a notification failure never corrupts the primary transaction.
Trap
The naive answer is 'listener exceptions are isolated'. Synchronous listeners propagate exceptions to the publisher. Another trap: assuming async listeners' failures are logged by default — unhandled async exceptions vanish unless you register an AsyncUncaughtExceptionHandler. Candidates who distinguish the sync-propagation and async-isolation paths show real understanding.