Spring Boot cookbook · Intermediate
Send emails after a transaction commits in Spring
Send a confirmation email only if the order was actually saved, without slowing down the request.
Approach
Publish a domain event inside the transactional service, handle it with @TransactionalEventListener(phase = AFTER_COMMIT) and @Async so it runs after a successful commit on another thread. For guaranteed delivery across crashes, use an outbox table instead.
Solution
public record OrderPlaced(long orderId, String email) {}
@Service
class OrderService {
private final OrderRepository orders;
private final ApplicationEventPublisher events;
OrderService(OrderRepository orders, ApplicationEventPublisher events) { this.orders = orders; this.events = events; }
@Transactional
public Order place(PlaceOrder cmd) {
Order order = orders.save(Order.from(cmd));
events.publishEvent(new OrderPlaced(order.getId(), cmd.email()));
return order;
}
}
@Component
class OrderConfirmationMailer {
private final JavaMailSender mail;
OrderConfirmationMailer(JavaMailSender mail) { this.mail = mail; }
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void on(OrderPlaced event) {
var msg = new SimpleMailMessage();
msg.setTo(event.email());
msg.setSubject("Order #" + event.orderId() + " confirmed");
msg.setText("Thanks for your order!");
mail.send(msg);
}
}
@Configuration
@EnableAsync
class AsyncConfig {}
Pitfalls
- Plain
@EventListenerruns before commit (emails for rolled-back orders). - Lost emails if the app crashes after commit — use an outbox for guarantees.
- Unbounded async executors (configure
spring.task.execution.pool.*).
Variations
- Spring Modulith's event publication registry persists events for at-least-once delivery.
- Send via a queue (RabbitMQ/SQS) consumed by a mailer service.
Further reading: Spring: Transaction-bound events
Reference: https://docs.spring.io/spring-framework/reference/data-access/transaction/event.html
More Spring Boot recipes
- Add correlation IDs to Spring Boot logs
- Add retries and a circuit breaker with Resilience4j
- Authenticate machine clients with API keys in Spring Security
- Cache service results in Redis with @Cacheable
- Consume Kafka messages with retries and a dead-letter topic
- Handle concurrent updates with optimistic locking
- Implement soft delete with Hibernate
- Make POST endpoints idempotent in Spring Boot