Application events: decoupling with @EventListener
Events let one part of the application announce that something happened without knowing who reacts:
- Publish with
ApplicationEventPublisher.publishEvent(event). An event can be any object; records are ideal. - Listen with
@EventListeneron a method of any bean. The parameter type decides which events it receives. - Listeners run synchronously by default: in the publisher's thread and transaction. If one throws, the exception reaches the publisher.
@TransactionalEventListenerruns after the transaction commits (by default), so emails and notifications never go out for rolled-back work.- Add
@Async(with@EnableAsync) to run a listener in the background. @Ordercontrols the order;condition = "#event.amount > 10000"filters events.- Spring publishes its own events too, such as
ApplicationReadyEventwhen the app has started.
Use events for side effects inside one application: emails, metrics, cache invalidation, audit logs. For communication between services, or when events must never be lost, use a message broker such as Kafka.
Example
public record OrderPlaced(long orderId, String email, long amountPaise) {}
@Service
class OrderService {
private final OrderRepository orders;
private final ApplicationEventPublisher events;
OrderService(OrderRepository orders, ApplicationEventPublisher events) { this.orders = orders; this.events = events; }
@Transactional
public void place(Order order) {
orders.save(order);
events.publishEvent(new OrderPlaced(order.id(), order.email(), order.totalPaise()));
// OrderService knows nothing about emails, metrics or loyalty points
}
}
@Component
class ReceiptEmails {
@Async
@TransactionalEventListener // only after the order is committed, on another thread
void send(OrderPlaced e) { mailer.sendReceipt(e.email(), e.orderId()); }
}
@Component
class SalesMetrics {
@EventListener // synchronous, inside the same transaction
void count(OrderPlaced e) { metrics.counter("orders.placed").increment(); }
}Common mistake
Sending emails from a plain @EventListener inside a transaction that later rolls back: the customer gets a receipt for an order that doesn't exist.
Under the hood
Events are in-memory: if the application crashes after the commit but before an async listener finishes, that work is lost. For must-not-lose side effects, use the transactional outbox pattern (write the event to a table in the same transaction and publish it afterwards) or Spring Modulith's event publication registry, which does that for you.
Check yourself
Which listener runs only if the publishing transaction commits?
How this connects
Know these first
Where this leads
You've reached the end of this thread. Try a learning path for what's next.
Was this lesson helpful?
Finished reading? Mark it complete to track your progress.