Databases, Spring and microservices · 2. Spring Core and Spring MVC, lesson 7 of 8

Application events: decoupling with @EventListener

Intermediate3 min read@since 17Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

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 @EventListener on 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.
  • @TransactionalEventListener runs 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.
  • @Order controls the order; condition = "#event.amount > 10000" filters events.
  • Spring publishes its own events too, such as ApplicationReadyEvent when 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.

Diagram

Example

Java
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

Where this leads

You've reached the end of this thread. Try a learning path for what's next.

Part of Spring Core and Spring Security in depth.

Was this lesson helpful?

Finished reading? Mark it complete to track your progress.