What are payload events, and how does Spring handle a plain object published as an event?
answer
- POJO event since Spring 4.2
- wrapped in PayloadApplicationEvent<T>
- getPayload / getSource
- ResolvableType generic matching
- ResolvableTypeProvider for erasure
basics
~10 sA 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 sBefore 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 linesrecord 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
Know that events can be plain objects, not just ApplicationEvent subclasses.
Explain PayloadApplicationEvent wrapping and declaring the payload type in the listener.
Discuss ResolvableType-based generic matching and immutability of payloads.
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