Skip to content
Spring Boot sb core 4 min read

Application Events

Spring’s eventing model lets beans communicate without holding direct references to one another. A publisher raises an event, and any number of listeners react to it. This in-process publish/subscribe mechanism keeps modules loosely coupled and is the idiomatic way to decouple side effects (sending email, clearing a cache, writing an audit record) from core business logic.

Why events?

Imagine an OrderService that, after placing an order, must send a confirmation email, update inventory, and notify analytics. Wiring all three collaborators into the service couples it to concerns that have nothing to do with placing an order. By publishing an OrderPlacedEvent instead, the service stays focused and new reactions can be added later without touching it.

Events are synchronous by default and run inside the publisher’s thread (and transaction), which makes them easy to reason about before you opt into asynchronous delivery.

Defining a custom event

Since Spring 4.2 an event can be any plain object — there is no need to extend the legacy ApplicationEvent base class. A record is the cleanest choice for an immutable event.

public record OrderPlacedEvent(String orderId, BigDecimal total) { }

Note: Extending ApplicationEvent is still supported but unnecessary. Use a record or a simple class unless you need the getTimestamp()/getSource() plumbing the base class provides.

Publishing events

Inject ApplicationEventPublisher and call publishEvent. The publisher is provided by the container, so constructor injection is all you need.

@Service
@RequiredArgsConstructor
public class OrderService {

    private final ApplicationEventPublisher events;

    public void placeOrder(String orderId, BigDecimal total) {
        // ... persist the order ...
        System.out.println("Order " + orderId + " saved");
        events.publishEvent(new OrderPlacedEvent(orderId, total));
        System.out.println("Returned from publishEvent");
    }
}

Listening with @EventListener

Annotate any bean method with @EventListener; the parameter type selects which events it receives.

@Component
public class OrderListeners {

    @EventListener
    public void sendConfirmation(OrderPlacedEvent event) {
        System.out.println("Emailing confirmation for " + event.orderId());
    }

    @EventListener
    public void updateInventory(OrderPlacedEvent event) {
        System.out.println("Reserving stock for " + event.orderId());
    }
}

Because default delivery is synchronous, every listener runs to completion before publishEvent returns:

Output:

Order A-100 saved
Emailing confirmation for A-100
Reserving stock for A-100
Returned from publishEvent

Tip: A listener may itself publish a new event — for example a method returning a non-null object has that return value published as a follow-up event automatically.

Conditional listeners

The condition attribute takes a SpEL expression evaluated against the event. The listener fires only when it resolves to true. The event is exposed as the root object via #root, and method parameters by name.

@EventListener(condition = "#event.total > 1000")
public void flagHighValue(OrderPlacedEvent event) {
    System.out.println("High-value order flagged: " + event.orderId());
}

Here flagHighValue runs only for orders above 1000. This keeps filtering logic declarative instead of scattering if checks across listener bodies.

Transactional listeners

A plain @EventListener runs inside the publisher’s transaction, so its side effects roll back if that transaction fails — and an email sent for an order that never committed is a real bug. @TransactionalEventListener defers execution until a specific transaction phase.

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendConfirmation(OrderPlacedEvent event) {
    System.out.println("Confirmation sent after commit for " + event.orderId());
}
PhaseFires when
AFTER_COMMIT (default)Transaction committed successfully
AFTER_ROLLBACKTransaction rolled back
AFTER_COMPLETIONTransaction completed (commit or rollback)
BEFORE_COMMITJust before the commit flushes

Warning: By default a @TransactionalEventListener is silently skipped if there is no active transaction. Set fallbackExecution = true if the listener should still run when none is present.

Asynchronous events

To run listeners off the publishing thread, enable async support and annotate the listener with @Async. The publisher returns immediately and the listener runs on a managed thread pool.

@Configuration
@EnableAsync
public class AsyncConfig { }
@Component
public class AnalyticsListener {

    @Async
    @EventListener
    public void track(OrderPlacedEvent event) {
        System.out.println("[" + Thread.currentThread().getName()
                + "] analytics for " + event.orderId());
    }
}

Output:

Order A-100 saved
Returned from publishEvent
[task-1] analytics for A-100

Notice that Returned from publishEvent now prints before the listener — delivery no longer blocks the caller. With async listeners, exceptions are logged rather than propagated to the publisher, so handle failures inside the listener.

See Asynchronous Execution for configuring the executor, return types, and exception handling in depth.

Built-in lifecycle events

Spring publishes its own events you can listen for, which is handy for startup wiring:

@EventListener
public void onReady(ApplicationReadyEvent event) {
    System.out.println("Application is ready to serve requests");
}

Common ones include ApplicationStartedEvent, ApplicationReadyEvent, and ContextRefreshedEvent.

Best Practices

  • Model events as immutable records; don’t extend ApplicationEvent without a reason.
  • Use @TransactionalEventListener(AFTER_COMMIT) for side effects that must not happen on rollback.
  • Keep synchronous listeners fast — they block the publisher. Move slow work to @Async.
  • Prefer the condition attribute over if checks inside listener bodies.
  • Remember async listeners swallow exceptions; log and handle them locally.
Last updated June 13, 2026
Was this helpful?