skip to content

Messaging vs In-Process Application Events

In-process @EventListener events and broker-backed messages solve related problems at very different costs, and @TransactionalEventListener sits on the boundary between them. Interviewers ask when a call should leave the process at all — a genuine design question.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

5

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

open as a page

How do @EventListener invocations behave with respect to threading and the publisher's transaction, and how do you change that?

level: middleimportance: must knowfreq 55%

basics

~20 s

By default a listener runs on the same thread, inside the publisher's transaction, and blocks the publisher until it finishes. Add @Async (with @EnableAsync) to run it on a separate thread, which also removes it from that transaction.

open as a page

Explain @TransactionalEventListener: what problem it solves, its phases, and why it's the common bridge from application events to a broker.

level: seniorimportance: must knowfreq 50%

basics

~20 s

@TransactionalEventListener delays a listener until the publisher's transaction reaches a chosen phase — usually AFTER_COMMIT. That way you only act (e.g. send a broker message) on data that actually committed, avoiding notifications about work that later rolled back.

open as a page

How do you decide between staying with in-JVM application events and moving to a broker-backed message channel? What are the trade-offs?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Use in-JVM events when producer and consumer live in the same app and you don't need durability. Move to a broker when the consumer is a separate process, or you need messages to survive crashes, buffer under load, retry, replay, or fan out to independent consumers.

open as a page

A team bridges domain events to Kafka with @TransactionalEventListener(AFTER_COMMIT). Occasionally the DB shows a change but no Kafka message was produced. Diagnose and design a fix.

level: principalimportance: should knowfreq 33%

basics

~20 s

AFTER_COMMIT runs only after the DB commits, but the Kafka send is a separate operation. If the app crashes or the broker is unavailable after commit but before the send succeeds, the message is lost. This dual-write gap is fixed with a transactional outbox.

open as a page