Why would you delay sending an email or publishing a message until after the database transaction commits, instead of doing it inline in your service method?
answer
- rollback can't un-send an email
- side effects aren't transactional
- wait until data is durable
- AFTER_COMMIT = truthful notification
- crash-after-commit still loses it
basics
~20 sBecause the transaction might still roll back. If you email or publish before commit and the transaction then fails, you have acted on data that was never saved. Waiting until after commit ensures the side effect only happens for data that actually persisted.
solid answer
~40 sA database transaction can still roll back after your code runs — a later step throws, a constraint fails, or commit itself errors. If you send an email or publish a Kafka message inline, that side effect already left the building and cannot be undone: you have notified about an order that does not exist. External side effects are not transactional, so you defer them to AFTER_COMMIT — the point where the data is guaranteed durable. In Spring you express this with an event plus @TransactionalEventListener(phase = AFTER_COMMIT), or by registering a TransactionSynchronization. The trade-off: if the app crashes after commit but before the side effect runs, the notification is lost — that residual gap is what the transactional outbox pattern later addresses.
code
java · 21 lines// BAD: side effect fires before the transaction is guaranteed to commit
@Transactional
public void placeOrder(Order order) {
orderRepository.save(order);
emailService.sendConfirmation(order); // if a later step rolls back,
// the email is already gone
inventoryService.reserve(order); // <-- may throw -> rollback
}
// GOOD: publish an event; deliver the email only AFTER_COMMIT
@Transactional
public void placeOrder(Order order) {
orderRepository.save(order);
inventoryService.reserve(order);
events.publishEvent(new OrderPlaced(order.getId()));
}
@TransactionalEventListener // phase defaults to AFTER_COMMIT
public void onOrderPlaced(OrderPlaced e) {
emailService.sendConfirmation(e.orderId());
}go deeper
Must grasp the core idea: a rollback cannot un-send an email, so wait until the data is committed.
Should name the concrete Spring mechanism (@TransactionalEventListener AFTER_COMMIT) rather than describing it vaguely.
Should articulate the residual crash-after-commit gap and gesture at the outbox pattern.
Frames this as one instance of the dual-write consistency problem and knows the boundary of what AFTER_COMMIT can and cannot guarantee.
## The core problem A relational transaction gives you atomicity **for the database only**. Everything you do to the outside world — sending an email, publishing to Kafka/RabbitMQ, calling a REST API, writing a file — is **not** part of that transaction and **cannot be rolled back**. Consider a naive service: 1. `orderRepository.save(order)` 2. `emailService.sendConfirmation(order)` ← side effect fires here 3. ...more work that throws an exception Spring's declarative transaction (`@Transactional`) will now roll back step 1 — the order is **not** in the database — but the confirmation email in step 2 has **already been sent**. The customer got a receipt for an order that does not exist. This is a classic **premature side effect** bug. ## The fix: run side effects AFTER_COMMIT You move the side effect to a hook that Spring fires **only after the transaction has successfully committed**. At that moment the data is durably persisted, so any notification about it is truthful. Two idiomatic Spring mechanisms: 1. **`@TransactionalEventListener`** — publish a domain event during the transaction; a listener annotated `@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)` (AFTER_COMMIT is the default phase) runs the side effect after commit. This is the clean, decoupled approach. 2. **`TransactionSynchronizationManager.registerSynchronization(...)`** — a lower-level callback API where you override `afterCommit()`. ## What 'after commit' guarantees — and does not - **Guarantees:** the transaction reached the durable state. Nothing your side effect sees will later vanish through rollback. - **Does NOT guarantee delivery:** if the JVM crashes, or the broker is down, in the window *between* commit and the side effect completing, the side effect is simply lost. Commit happened; the email did not. Spring does not retry it for you. This narrower failure mode is the **dual-write problem**, solved separately by the **transactional outbox** pattern (persist the intent to send inside the same transaction, deliver it asynchronously from that table). ## When to use Use AFTER_COMMIT for **any non-transactional side effect that must reflect committed data**: notifications, message/event publication, cache invalidation of external caches, triggering downstream workflows. Do **not** use it for work that must be atomic with the data — that belongs inside the transaction. ## Junior-friendly rule of thumb > Anything you cannot un-send should wait until the data is safely saved.
- If the transaction rolls back, does the AFTER_COMMIT listener still run?No. AFTER_COMMIT listeners only fire on a successful commit. On rollback they are skipped entirely (an AFTER_ROLLBACK phase exists separately if you need that).
- Is the problem fully solved once you move the email to AFTER_COMMIT?Mostly, but not completely. You have eliminated false notifications on rollback. A residual gap remains: if the app crashes between commit and sending, the email is lost — that's the dual-write problem the outbox pattern addresses.
saying these in an interview costs you the question
- Claiming the email can be 'rolled back' along with the database transaction — external side effects are not transactional.
- Believing AFTER_COMMIT guarantees the side effect will definitely be delivered (it doesn't survive a crash after commit).
- Thinking the listener runs even on rollback.