Careeroza
Transactional Event Listeners
The Problem
If the email listener runs inside the order transaction and the transaction later rolls back (for example, stock check fails), the customer has already received "Order confirmed" for an order that does not exist.
@TransactionalEventListener waits for the transaction outcome before running.
Phases
@Component
@Slf4j
public class OrderEventsHandler {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) // default
public void emailAfterCommit(OrderPlacedEvent e) {
log.info("Order {} committed, sending email", e.orderId());
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void alertOnRollback(OrderPlacedEvent e) {
log.warn("Order {} rolled back", e.orderId());
}
}| Phase | Runs when |
|---|---|
BEFORE_COMMIT | just before commit (still inside the transaction) |
AFTER_COMMIT | after a successful commit (default) |
AFTER_ROLLBACK | after a rollback |
AFTER_COMPLETION | after commit or rollback |
Gotchas
- If there is no active transaction, the listener does not run (unless
fallbackExecution = true). - In
AFTER_COMMITthe transaction is finished: database writes there need@Transactional(propagation = REQUIRES_NEW). - Events live only in memory. If the app crashes right after commit, the event is lost. For guaranteed delivery use the outbox pattern (see the Microservices material).
Practice
Exercise: Write a listener that clears the user's cart only after the order transaction commits.
Show answer
@Component
@RequiredArgsConstructor
public class CartCleanup {
private final CartItemRepository cartItems;
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void clearCart(OrderPlacedEvent e) {
cartItems.deleteByUserId(e.userId());
}
}