skip to content

What are payload events, and how does Spring handle a plain object published as an event?

level: middleimportance: should knowfreq 45%

answer

  1. POJO event since Spring 4.2
  2. wrapped in PayloadApplicationEvent<T>
  3. getPayload / getSource
  4. ResolvableType generic matching
  5. ResolvableTypeProvider for erasure

basics

~10 s

A payload event is any plain object you publish without extending ApplicationEvent. Spring wraps it in a PayloadApplicationEvent<T> internally, but your @EventListener method just declares the payload type as its parameter.

solid answer

~40 s

Before Spring 4.2 an event had to extend ApplicationEvent. Since 4.2 you can publish any object — a 'payload event'. Internally Spring wraps it in PayloadApplicationEvent<T>, whose getSource() is the publisher and getPayload() is your object. Your listener normally just declares the payload type (e.g. @EventListener void on(OrderPlaced e)) and Spring unwraps it. If you need the source or the wrapper, you can declare the parameter as PayloadApplicationEvent<OrderPlaced>. Generic payloads participate in type matching: a listener for PayloadApplicationEvent<String> only fires for String payloads, thanks to ResolvableType-based resolution. This lets you use immutable records/DTOs as events with zero Spring coupling in the event class itself, which is why modular codebases favor them.

code

java · 17 lines
java
record OrderPlaced(long orderId) {}

@Component
class OrderListeners {
    // Unwrapped: just declare the payload type
    @EventListener
    void onOrder(OrderPlaced event) {
        System.out.println("Order " + event.orderId());
    }

    // Wrapped: when you need source/timestamp
    @EventListener
    void onWrapped(PayloadApplicationEvent<OrderPlaced> event) {
        Object source = event.getSource();
        OrderPlaced payload = event.getPayload();
    }
}

go deeper

for a junior

Know that events can be plain objects, not just ApplicationEvent subclasses.

for a middle

Explain PayloadApplicationEvent wrapping and declaring the payload type in the listener.

for a senior

Discuss ResolvableType-based generic matching and immutability of payloads.

for a principal

Address erasure edge cases (ResolvableTypeProvider) and why POJO events suit modular boundaries.

## The problem payload events solve Originally, a Spring event had to subclass `org.springframework.context.ApplicationEvent`, which forces a constructor taking a `source` object and couples your domain type to Spring. Spring 4.2 removed that requirement: you can publish **any** object. ## What actually happens When you call `publisher.publishEvent(myPojo)` and `myPojo` is **not** an `ApplicationEvent`, Spring wraps it in a `org.springframework.context.PayloadApplicationEvent<T>`: - `getPayload()` returns your object. - `getSource()` returns the publisher (the ApplicationContext or the object that published, depending on how it was published). - `getTimestamp()` is set. If you publish something that already **is** an `ApplicationEvent`, no wrapping occurs. ## How listeners match A listener declares the **payload type** directly and Spring unwraps for you: ```java @EventListener void on(OrderPlaced event) { ... } // receives the payload ``` If you need the wrapper (e.g., the source or timestamp), declare it explicitly: ```java @EventListener void on(PayloadApplicationEvent<OrderPlaced> event) { OrderPlaced payload = event.getPayload(); Object source = event.getSource(); } ``` ## Generic type matching Spring resolves listener parameter generics via `ResolvableType`. So a `@EventListener void on(PayloadApplicationEvent<String> e)` fires only for `String` payloads, not `Integer`. With raw payload-type listeners the ordinary Java type (and its supertypes/interfaces) is matched — a listener for `Object` catches everything. A subtlety: generic **erasure** can defeat matching when the payload's own type is generic (e.g. publishing a `List<String>`), because the runtime object cannot always tell `List<String>` from `List<Integer>`. For those cases you can implement `ResolvableTypeProvider` on the event to advertise its full generic type. ## Why this matters - **Zero coupling:** your event can be a Kotlin `data class` / Java `record` with no Spring imports — ideal for domain events in a Spring Modulith app. - **Immutability:** payloads are usually immutable value objects, which is safer when several listeners read them. - **Interop:** old `ApplicationEvent` subclasses still work side by side. ## Gotchas - Do not mutate a payload inside a listener expecting other listeners not to see it — they share the same instance. - If a raw-generic payload isn't delivered, suspect erasure and use `ResolvableTypeProvider`. - `getSource()` on a wrapped payload is the publisher, which is often the context, not your domain object — don't rely on it for business data.

  • How can a listener access the wrapper instead of the raw payload?
    Declare the parameter as PayloadApplicationEvent<YourPayload>; Spring will inject the wrapper, giving you getPayload(), getSource(), and getTimestamp().
  • Why might a List<String> payload not reach a PayloadApplicationEvent<List<String>> listener?
    Generic type erasure hides the element type at runtime. Implement ResolvableTypeProvider on the event, or wrap it in a concrete class, so Spring can resolve the full generic type.

saying these in an interview costs you the question

  • Claiming plain-object events must be serialized
  • Not knowing the PayloadApplicationEvent wrapper exists
  • Assuming generic payloads always match regardless of erasure

context