@TransactionalEventListener vs @Async Event Listeners
Fast recall
Quick revision
Core answer
AFTER_COMMIT, BEFORE_COMMIT, AFTER_ROLLBACK, AFTER_COMPLETION) so the listener only runs at that point relative to the publisher's transaction. `@Async` runs the listener on a separate thread pool, fully decoupling execution timing from the publisher. They solve different problems: @TransactionalEventListener is about when the listener fires (must be after commit); @Async is about where it fires (different thread). You can combine both: an @Async @TransactionalEventListener(AFTER_COMMIT) that sends an email asynchronously only after the order commits successfully — the standard production pattern for non-critical side effects.Whether the candidate understands the difference between transaction-phase binding and thread-pool decoupling — two orthogonal concerns that are commonly conflated.
Assuming @Async alone is enough to safely send an email after a save — without @TransactionalEventListener, an @Async listener can fire before the transaction commits and email about an order that later rolls back.
Discuss the silent-discard trap of @TransactionalEventListener with no active transaction, the SimpleAsyncTaskExecutor default thread-pool danger, and the outbox pattern as the durable alternative for cross-service events.
Interview-ready guide
Interview answer
These two annotations solve different problems and are often conflated. The cleanest mental model is: @TransactionalEventListener controls when a listener runs relative to the publisher's transaction; @Async controls where it runs, on which thread.
`@TransactionalEventListener`
- Binds the listener to a specific transaction phase:
AFTER_COMMIT(default),BEFORE_COMMIT,AFTER_ROLLBACK, orAFTER_COMPLETION - Same thread by default — does not give you concurrency, just timing
- Trap: if published outside a transaction, the listener is silently discarded; add
fallbackExecution = trueif both modes are safe
`@Async`
- Runs the listener on a separate thread pool, decoupling execution from the publisher's thread
- No awareness of transaction phase — an
@Asynclistener can start before the publisher's transaction commits, which is dangerous for side effects - Default
SimpleAsyncTaskExecutorcreates unbounded threads — must be replaced with a boundedThreadPoolTaskExecutor
The production combination
@Async @TransactionalEventListener(AFTER_COMMIT)— fires only after commit, on a worker thread- The standard pattern for emails, notifications, and cache invalidation
- Doesn't block the API response; doesn't fire on rollback
Outbox for durability across services
- Spring Events are in-process best-effort — they don't survive a JVM crash between commit and broker publish
- For cross-service or at-least-once delivery, write an outbox row in
BEFORE_COMMITso it shares the transaction; CDC or a poller publishes to Kafka
If the interviewer pushes on context propagation, mention that SecurityContextHolder and MDC don't cross thread boundaries — use DelegatingSecurityContextAsyncTaskExecutor and a TaskDecorator for MDC. This is the gap that breaks audit logs in async listeners.
@TransactionalEventListener vs @Async Event Listeners
Direct Answer
@TransactionalEventListener binds a listener to a specific transaction phase (AFTER_COMMIT, BEFORE_COMMIT, AFTER_ROLLBACK, AFTER_COMPLETION) so the listener only runs at that point relative to the publisher's transaction. @Async runs the listener on a separate thread pool, fully decoupling execution timing from the publisher. They solve different problems: @TransactionalEventListener is about when the listener fires (must be after commit); @Async is about where it fires (different thread). You can combine both: an @Async @TransactionalEventListener(AFTER_COMMIT) that sends an email asynchronously only after the order commits successfully — the standard production pattern for non-critical side effects.
Common Mistakes & Pitfalls
Assuming @Async alone is enough to safely send an email after a save — without @TransactionalEventListener, an @Async listener can fire before the transaction commits and email about an order that later rolls back.
Check your understanding
Answer these without looking back at the article.
- What happens if you put @Async on a @TransactionalEventListener but no executor bean is configured?
- How do you propagate the security context and MDC to an @Async event listener?
- Why does fallbackExecution=true exist on @TransactionalEventListener?
- When would you choose the outbox pattern over @Async @TransactionalEventListener?
Related interview prep categories
1Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.