Skip to content

Spring Boot · Async & Messaging

Spring Boot Application Events: In-Process Pub/Sub Done Right

5 min read Updated 2026-06-26 Share:

Practice Application Events interview questions

Decoupling beans without a broker

Every ApplicationContext is also a tiny publish/subscribe bus. One bean publishes an event; any number of listeners react to it; and crucially, the publisher holds no reference to the listeners. That's the whole point — an OrderService can announce "an order was placed" without knowing that an email sender, an audit logger, and an inventory updater all care. Add a fourth reaction later and the publisher never changes.

@Service
class OrderService {
    private final ApplicationEventPublisher publisher;
    OrderService(ApplicationEventPublisher p) { this.publisher = p; }

    void place(Order o) {
        save(o);
        publisher.publishEvent(new OrderPlacedEvent(o.id()));  // who listens? not my problem
    }
}

Since Spring 4.2 the event can be any plain object — no base class, no registration. A record is perfect.

Listening is just an annotated method

Subscribe by annotating a method with @EventListener and declaring the event type as its single parameter. Spring registers it and invokes it whenever a matching event fires:

@Component
class EmailListener {
    @EventListener
    void on(OrderPlacedEvent e) {       // the parameter type IS the subscription
        sendConfirmation(e.orderId());
    }
}

One bean can host many listeners, each for a different event type. You can narrow which events a listener cares about with a SpEL condition, so the body only runs when it matters:

@EventListener(condition = "#event.total > 1000")
void onLargeOrder(OrderPlacedEvent event) {     // only high-value orders
    flagForReview(event);
}

The "events are async" myth

The single biggest misconception: by default, listeners are synchronous and run on the publisher's thread. publishEvent does not return until every listener has finished, sequentially, in the same call stack. So a slow listener slows the publisher, and an exception in a sync listener propagates back to the publisher and aborts the remaining listeners. Out of the box, an "event" is really just a decoupled method call.

To actually leave the publisher's thread, add @Async (with @EnableAsync):

@Async
@EventListener
void on(OrderPlacedEvent e) {       // now runs on an executor; publishEvent returns at once
    slowAuditWrite(e);
}

But async listeners come with strings attached: they can't propagate exceptions back to the publisher, and because they run on a different thread they lose the thread-bound transaction and security context. The SecurityContextHolder is empty inside an async listener unless you propagate it deliberately or carry what you need inside the event payload.

The transaction trap, and how @TransactionalEventListener fixes it

Here's a bug that bites everyone eventually. You publish "order placed", an @Async listener fires off a confirmation email — and then the surrounding transaction rolls back. Now you've emailed a customer about an order that doesn't exist. The fix is @TransactionalEventListener, which binds the listener to a transaction phase and (by default) only fires after the transaction commits:

@TransactionalEventListener            // default phase: AFTER_COMMIT
void on(OrderPlacedEvent e) {
    sendConfirmation(e);               // only runs if the data was actually saved
}

Other phases exist (BEFORE_COMMIT, AFTER_ROLLBACK, AFTER_COMPLETION), but AFTER_COMMIT is the workhorse: side effects that must only happen if the write was real. One gotcha — if there's no active transaction, the listener is silently skipped. That's why it can mysteriously not fire in a test that published outside a transaction. Set fallbackExecution = true to make it run anyway.

Ordering, chaining, and built-in events

For synchronous listeners that have a dependency, control their order with @Order (lower runs first); without it, order is undefined. A listener can even return a value, which Spring publishes as a new event — handy for chaining, easy to over-use:

@Order(1) @EventListener void validateFirst(OrderPlacedEvent e) { }
@Order(2) @EventListener void thenNotify(OrderPlacedEvent e) { }

Spring Boot also publishes its own lifecycle events. The most useful is ApplicationReadyEvent, fired once when the app is fully started and ready to serve traffic — the right hook for one-time startup work:

@EventListener(ApplicationReadyEvent.class)
void warmCaches() {                  // runs once, after the context and web server are up
    preloadReferenceData();
}

Where events stop being the answer

Application events are in-memory and single-JVM. They vanish on restart, never cross a process boundary, and offer no persistence or delivery guarantees. They're for decoupling beans inside one app. The moment you need a consumer in another service, durability, or at-least-once delivery, you need a real broker — Kafka or RabbitMQ — not the event bus. And the choice between an event and a plain method call is about coupling: one known collaborator, call it directly for clarity and traceability; many optional or cross-cutting reactions, publish an event.

Rule of thumb: Use application events to decouple beans within a process, make them @Async or @TransactionalEventListener when timing matters — and reach for a broker the moment the reaction lives in another service.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel