skip to content

What is the difference between Spring's ApplicationEventPublisher / @EventListener and the messaging support in spring-messaging (Message / MessageChannel)?

level: juniorimportance: must knowfreq 60%

answer

  1. @EventListener = in-JVM pub/sub, same thread + tx
  2. spring-messaging = Message/MessageChannel, broker-backed, cross-process
  3. events not durable, not serialized
  4. @TransactionalEventListener bridges to broker
  5. sync by default, not async

basics

~10 s

ApplicationEventPublisher with @EventListener sends events between beans inside one running JVM. spring-messaging (Message, MessageChannel) is a broader abstraction for passing messages, often across process boundaries through a broker like Kafka or RabbitMQ.

solid answer

~40 s

@EventListener plus ApplicationEventPublisher is the ApplicationContext's in-process pub/sub: a bean publishes a plain object, other beans in the same JVM handle it, synchronously by default, sharing the caller's thread and transaction. Nothing leaves the process and nothing is serialized. spring-messaging is a generic messaging abstraction — Message<T> (payload + headers), MessageChannel, MessageHandler — that underpins Spring Integration, STOMP/WebSocket, and Spring Cloud Stream, and is typically wired to a real broker so messages cross process boundaries, are serialized, and can be durable. Rule of thumb: use application events for decoupling components within one app; reach for messaging/a broker when the consumer is a separate process or you need durability, buffering, or delivery guarantees. @TransactionalEventListener is the common bridge between the two worlds.

code

java · 20 lines
java
// In-process application event
record OrderPlaced(String orderId) {}

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

    @Transactional
    public void place(String id) {
        // ... persist order ...
        publisher.publishEvent(new OrderPlaced(id)); // in-JVM, same thread + tx
    }
}

@Component
class InventoryListener {
    @EventListener // synchronous by default; runs in OrderService's transaction
    void on(OrderPlaced e) { /* same JVM, no serialization */ }
}

go deeper

for a junior

Know one is inside the app (events), the other can go between apps via a broker (messaging).

for a middle

Add that events are synchronous, share the transaction, and are not serialized or durable.

for a senior

Articulate the decision boundary and name @TransactionalEventListener as the bridge to a broker.

for a principal

Frame it as coupling/lifecycle strategy and note the dual-write reliability limit that motivates an outbox.

## Two different tools that look similar Spring gives you two ways to send a notification from producer code to consumer code without them calling each other directly. They are easy to confuse but solve different problems. ### 1. In-process application events - **`ApplicationEventPublisher`** — an interface every Spring bean can inject (the `ApplicationContext` implements it). You call `publishEvent(someObject)`. - **`@EventListener`** — put on any bean method; the method's single parameter type selects which events it receives. (Older style: implement `ApplicationListener<T>`.) - Since Spring 4.2 the event can be **any object** — it no longer has to extend `ApplicationEventPublisher`'s legacy `ApplicationEvent`. **Key semantics:** - **Synchronous by default.** `publishEvent` does not return until every listener has run. Listeners execute on the **same thread** and inside the **same transaction** as the publisher. - **In-JVM only.** The event object is passed by reference — no serialization, no network. If the JVM crashes, the event is gone. - Ordering via `@Order`; async via `@Async` on the listener (needs `@EnableAsync`) or by configuring the `ApplicationEventMulticaster` with a `TaskExecutor`. ### 2. spring-messaging abstraction The `spring-messaging` module defines broker-neutral types: - **`Message<T>`** — a payload (`getPayload()`) plus a `MessageHeaders` map. - **`MessageChannel`** — something you `send(Message)` to; a `SubscribableChannel` delivers to `MessageHandler`s. - **`MessageHandler`** — receives messages. These types are the foundation for **Spring Integration**, **STOMP messaging over WebSocket** (`SimpMessagingTemplate`), and **Spring Cloud Stream**. In real systems the channel is backed by a **broker** (Kafka, RabbitMQ, JMS), so messages leave the process, are **serialized**, can be **buffered and made durable**, and can be consumed by a **different application**. ## How they relate — the mental model - Application events = wiring **inside** one deployable. - Broker-backed messaging = wiring **between** deployables (or for durability/async offloading even within one). - Both are pub/sub, but only messaging crosses the process boundary and survives a restart. ## When to cross the process boundary Stay with application events when: the handler lives in the same app, you want to decouple modules, you don't need durability, and eventual work can happen in-thread or via `@Async`. Move to a broker when: the consumer is a separate service, you need the message to survive a crash, you need back-pressure/buffering, ret/replay, or fan-out to many independent consumers, or you must decouple deployment lifecycles. ## The bridge: @TransactionalEventListener A very common pattern is to keep the producer decoupled by publishing an **in-process** application event, and have a `@TransactionalEventListener` (phase `AFTER_COMMIT`) turn that event into a **broker** message once the database transaction that produced it has committed. This avoids sending a message about data that later rolls back. The dual-write reliability caveat (DB commit succeeds, broker publish fails) is what pushes serious systems toward the transactional-outbox pattern. ## Gotchas - People assume `@EventListener` is async — it is **not** by default. - People assume application events are durable — they are **not**; a crash loses them. - `Message`/`MessageChannel` by themselves are just abstractions; without a broker binder they are still in-JVM (e.g. a plain `DirectChannel`).

  • Is publishEvent synchronous or asynchronous by default?
    Synchronous. It blocks until all listeners finish, on the publisher's thread and within its transaction. You make it async with @Async on the listener (plus @EnableAsync) or by giving the ApplicationEventMulticaster a TaskExecutor.
  • If a listener must notify a separate microservice, which approach fits?
    A broker-backed message (spring-messaging via Spring Cloud Stream / Kafka / RabbitMQ), because the consumer is another process and you need serialization and durability — application events cannot leave the JVM.

saying these in an interview costs you the question

  • Claiming @EventListener is asynchronous by default
  • Thinking application events are durable / survive a JVM crash
  • Believing ApplicationEventPublisher can deliver to another process/service
  • Saying Message/MessageChannel always means a network broker

context