CodeGym /Courses /Spring Core /Events: ApplicationEvent and payload

Events: ApplicationEvent and payload

Spring Core
Level 17, Lesson 1
Available

1. Event as a data model

If, in your head, an event looks like “well, we’ll just call something else instead of auditService.recordCreated()”, then you are almost guaranteed to drag old problems into the event model: too much data, too much coupling, and too little clarity. A good application event is not a replacement for a specific method, but a description of a fact that has already happened. And that fact should be understandable without knowing who will subscribe to it.

Imagine that you are making an announcement in the office: “Order #123 has been created.” That is news. It does not say “Marina, go send an email,” “Petya, log the audit,” and “Sasha, update the statistics.” The news simply records reality. And after that, each department has its own work. In code, we want exactly the same thing: an event should be a neutral carrier of information about what happened, without an “imperative tone” and without knowledge of specific handlers.

Another important idea for a beginner: an application event in our course is not a message in Kafka and not an email in RabbitMQ. It is an ordinary Java object that lives in memory within a single process. It is not serialized, does not “fly over the network,” and is not stored in a queue. Therefore, the requirements for it are closer to the requirements for a “good data model” than to the requirements for a “network contract.”

To fix the boundary, it is useful to compare two concepts from our project:

  • CreateOrderCommand is a command: “create an order with these data.”
  • OrderCreatedEvent is a fact: “the order has been created, here is the identifier and context.”

A command is an input to the scenario. An event is the trace the scenario leaves behind when the fact has already happened.

2. Payload and immutability

When a developer first starts creating events, the hand reaches out to put “everything” into them. For example, the whole Order, then Customer, then a list of OrderItem, and, on top of that, Locale and MessageSource (just in case the handler needs them). A week later, you discover that the event has become a suitcase without a handle: it drags half the application along with it, and changes in the domain model start breaking listeners in unexpected places.

The right intuition is this: an event should contain the minimum amount of data sufficient for the handlers, but at the same time should not force the handler to “guess” what happened. In our ContextFlow, the key handlers (audit and notifications) naturally work from the order identifier. They do not have to receive the entire order object if they can use orderId to generate a message or record a fact. The less data you pass, the lower the risk that someone will start using an “extra field just because it is there.”

Another topic is immutability. Inside one application, the event object is passed by reference. If an event can be changed (there are setters or mutable collections), then one handler may accidentally affect another. It sounds like a horror movie scenario: “the audit suddenly changed the cancellation reason, and the notification went out with a different reason.” In reality, that happens too — the developer just then stares at the debugger for a long time and quietly suffers.

Therefore, the basic rules for events today are simple and very practical:

  1. an event should have final fields set in the constructor;
  2. an event should not have setters;
  3. it is better to keep simple values in the event (id, reason, timestamp) rather than “live” services or half the domain;
  4. the event name should be in the past tense and read like a fact.

To make this concrete, let us agree right now on two facts that we will describe in ContextFlow:

  • order created (OrderCreated...)
  • order cancelled (OrderCancelled...)

And now we choose the form in which these facts will live as Java objects.

3. ApplicationEvent

ApplicationEvent is historically the “native” Spring base class for events. It immediately tells the reader: “this is not just a data model, this is an event for the Spring container.” For a learning day, that is even a plus: we argue less about style and see the mechanics faster. But it is important to honestly acknowledge the downside: this is framework coupling. Your domain starts depending on a Spring type, and this decision needs to be made consciously.

There are two things in ApplicationEvent that are useful for a beginner to know. First, the event always has a source — the source object (who published it). Second, the event has a timestamp (the internal time the event was created). In most application cases, we do not use source and do not build logic on the timestamp from the base class, but it is useful to know that they are there: you will not be surprised when you see them in the debugger.

OrderCreatedEvent as an ApplicationEvent

Let us start with a simple event: an order has been created. We will put in the minimally sufficient data that reactions definitely need: orderId and customerId. This will allow the auditor to record “order created,” and the notification handler to send a message to the customer (or choose text by Locale) without knowing the details of the scenario service.

package com.example.contextflow.domain.events;

import org.springframework.context.ApplicationEvent;

// Spring event: we extend ApplicationEvent so the container clearly sees "this is an event"
public class OrderCreatedEvent extends ApplicationEvent {

    // Minimal payload: identifiers instead of the "whole domain"
    private final String orderId;
    private final String customerId;

    public OrderCreatedEvent(Object source, String orderId, String customerId) {
        super(source); // source — who published the event (usually this from the service)
        this.orderId = orderId;
        this.customerId = customerId;
    }

    // There are getters, no setters — the event is immutable
    public String getOrderId() { return orderId; }
    public String getCustomerId() { return customerId; }
}

Notice a “grown-up” detail: we do not create setOrderId(...), we do not leave fields public, and we do not allow the event to be “half-built.” An event is either created correctly or it does not exist at all. This pairs very well with the mindset that a fact either happened or did not happen.

OrderCancelledEvent as an ApplicationEvent

Now for cancellation. Here, the minimally sufficient payload usually includes the order identifier and the cancellation reason. The reason is a classic example of a field that is convenient to have directly in the event: the notification can show it to the user, and the audit can store it as part of the record.

package com.example.contextflow.domain.events;

import org.springframework.context.ApplicationEvent;

// An order cancellation event: same approach, different fact
public class OrderCancelledEvent extends ApplicationEvent {

    private final String orderId;
    private final String reason;

    public OrderCancelledEvent(Object source, String orderId, String reason) {
        super(source); // we fix "who announced the fact", but do not use this as a service locator
        this.orderId = orderId;
        this.reason = reason;
    }

    // No setters: handlers must not "correct" the fact
    public String getOrderId() { return orderId; }
    public String getReason() { return reason; }
}

Yes, someone will definitely say: “And let’s add cancelledAt, actorId, previousStatus, newStatus...”. And sometimes that really is needed. But today we keep the model simple: an event is a learning “envelope” that carries a fact between parts of the application. The lighter this envelope is, the easier it is to move forward later.

source in ApplicationEvent

The source field in ApplicationEvent often confuses beginners: “What should I pass there? If I pass this, is that okay? What if I pass a string? What if I pass a cat?” Spoiler: a cat, of course, boosts team morale, but in production it is better without one.

The practical recommendation in the course is this: pass into source the object that publishes the event. Usually this is this from the scenario service. That does not mean listeners should call methods on source (that is exactly what you should not do). It is simply the “author signature” on the fact, which can be seen in logs or the debugger.

If you are writing a test or demo code, you can also pass new Object() — the mechanics do not care. What matters is something else: source should not become a way to “smuggle” services or context into the event. As soon as a handler starts using event.getSource() as a service locator, you are back to bad design, only now with the nice word “event.”

4. Option 2: a payload event

In modern versions of Spring (and this has long been the norm), you can publish any object as an event. This is called a payload event: you do not extend ApplicationEvent; you simply create an ordinary data model and pass it to publishEvent(...). Inside, the container wraps the payload in its own internal representation and delivers it to handlers.

Why do many people like this option? Because it looks like “honest Java”: the data object remains a data object, without framework dependencies. You literally get back the feeling that domain facts belong to the domain, not to the container.

A payload event as a record

If you are familiar with Java records (and in Java 25 they have long been an “ordinary tool”), then payload events fit the record style perfectly: it is compact, immutable, and readable. For a beginner, a record is almost a “data class without unnecessary noise.”

package com.example.contextflow.domain.events;

// Payload event: it is just data, Spring can publish any object
public record OrderCreatedPayload(String orderId, String customerId) {
    // A record is immutable by default: components are set in the constructor, there are no setters
}

And for cancellation:

package com.example.contextflow.domain.events;

// A separate type for a separate fact: less if/else in handlers
public record OrderCancelledPayload(String orderId, String reason) {
}

Records automatically have a constructor, equals/hashCode, toString, and getters in the form of methods orderId() and reason(). In other words, you get “immutability by default” and minimal code, without resorting to Lombok (which, as a reminder, is not and will not be in the course).

A payload event as a final class

If record is already familiar to you, this option is usually enough. But a payload event is not a record feature; it is simply a data object, so it can just as easily be made into an ordinary class. If records still seem “suspiciously short” to you (a normal reaction: the brain expects magic to be hidden somewhere), this variant is even more psychologically comfortable.

package com.example.contextflow.domain.events;

// A plain data class without Spring inheritance: also a payload event
public final class OrderCreatedPayload {

    private final String orderId;
    private final String customerId;

    public OrderCreatedPayload(String orderId, String customerId) {
        this.orderId = orderId;         // we fix the fact in the constructor
        this.customerId = customerId;   // we keep the same minimal payload as in the record variant
    }

    // Read-only: handlers receive the fact, but cannot "adjust" it
    public String getOrderId() { return orderId; }
    public String getCustomerId() { return customerId; }
}

It is a little more verbose, but the meaning is the same: an ordinary Java object, without extends ApplicationEvent, and with the same payload — orderId and customerId. What matters here is the principle itself, not a war between “records vs classes.”

5. Choosing the event form

In real life, the choice between ApplicationEvent and a payload event is rarely about “right/wrong.” It is usually about what you value more: the transparency of the mechanics and explicit Spring semantics, or the loose coupling of the domain with the framework. In a learning project, we add a third factor: “what is easier to explain to a beginner so that there is no feeling of magic.”

To make a conscious choice, it is useful to keep a small comparison table in mind:

Criterion extends ApplicationEvent Payload event (any object)
Coupling with Spring Yes: the domain depends on a Spring type Less: it is plain Java
Noise/verbiage A bit more code (source + constructor) Usually less, especially with record
Clarity of “this is an event” Very explicit: immediately visible You need to agree on naming/package conventions
Ease of learning Clearly shows the mechanics It may seem like ApplicationEvent “disappeared somewhere”
Model flexibility There are almost no restrictions, but inheritance sets the form Maximum freedom

In ContextFlow, it is reasonable right now to stick to one working line: we model business facts as descendants of ApplicationEvent. This form gives the most direct link “event object → publication → typed handling,” and it is the easiest way to see the mechanics without extra intermediate layers.

It is also useful to know payload events as an alternative, so that you do not think of ApplicationEvent as the only possible form. But the current project code does not rely on them: here we care more about seeing publishing-side and listener-side mechanics stably on the same contract.

publishEvent(...) for both forms

It is important to see that on the “publishing” side, Spring provides a single API: publishEvent(...). The container does not care whether it receives an ApplicationEvent descendant or “just an object.” For it, this is still an event. The difference is in how exactly you described the data model.

Here is a demonstration code fragment: not the final integration into the service, just a feeling for the API.

import org.springframework.context.ApplicationEventPublisher;

public class Demo {

    public void demo(ApplicationEventPublisher publisher) {
        // Publish a "classic" Spring event (ApplicationEvent descendant)
        publisher.publishEvent(new OrderCreatedEvent(this, "ord-1", "cust-7"));

        // Publish a payload event (any object, for example a record)
        publisher.publishEvent(new OrderCreatedPayload("ord-1", "cust-7"));
    }
}

This snippet is useful precisely as a “reality check”: one method, two forms, and then it is a matter of architecture and readability. For today’s lecture, it is enough to fix that Spring does not force you to extend ApplicationEvent, but also does not forbid it — the choice remains yours.

6. Package and event naming

When a project grows, the most unpleasant “technical” problem is not code, but chaos in the structure. Events are an excellent candidate for becoming chaos: they are easy to scatter across different packages, and even easier to name in a way that no one understands whether it is a fact or a request. That is why we establish discipline in advance.

In our project, there is a domain.events package, and that is exactly where application events should live. At the structural level, it reads as “domain facts that can be announced outward within the application.” It is not a DTO layer, not API contracts, and not a storage model — it is just events that other parts of the application can react to.

An example of the target file layout (simplified, but correct in meaning):

com.example.contextflow
└── domain
    └── events
        ├── OrderCreatedEvent.java
        └── OrderCancelledEvent.java

Now about naming. It is important that the event reads like a past fact. OrderCreatedEvent sounds like “order created.” But CreateOrderEvent sounds like the command “create an order” and will almost certainly lead to confusion between an event and a command. Even worse — NotificationEvent or AuditEvent: this is not a fact, it is already a reaction, which means you are swapping cause and effect.

If you want to specify the context in the name, do it so that the fact remains a fact. For example, OrderCreationFailedEvent is also a fact: “order creation failed.” But today we keep it minimal: created/cancelled.

7. Typical mistakes when designing events

Mistake #1: naming the event like a command or like a side effect.
When an event is named CreateOrderEvent or SendNotificationEvent, it stops being a fact and turns into an “order” or part of a specific reaction. As a result, the scenario service seems to “call a remote method” instead of announcing a fact. A proper event sounds like news: OrderCreatedEvent, OrderCancelledEvent.

Mistake #2: putting the entire domain object into the event “just in case.”
It is very tempting to put the whole Order into the event so that “it is more convenient in the handler.” But along with convenience, you bring risk: handlers start depending on every order field and break when the model changes even a little. It is even worse if Order is mutable: one handler can corrupt data for another. For the learning ContextFlow, most often orderId and several simple fields are enough.

Mistake #3: making the event mutable (setters, public fields, collections).
If an event has setters, you turn a fact into a draft. Inside a single process, the event object can pass through several handlers, and any change becomes “whoever was last is right.” Immutability here is not aesthetics; it is protection from very strange bugs.

Mistake #4: starting to use source as a way to smuggle in logic.
source in ApplicationEvent is a signature, not a “secret link to a service.” As soon as a handler does something like ((OrderPlacementService) event.getSource()).something(), you are creating a service locator under the guise of events. This increases coupling even more than a direct method call, because now the dependencies are also hidden in runtime casts.

Mistake #5: creating one overly general event type “for everything.”
OrderChangedEvent seems convenient until you add a second order-change scenario. Then handlers turn into a forest of if/else and manual checks of “what exactly changed?” To start, it is better to have separate events for specific facts. In our day, there are exactly two: created and cancelled — and that is already enough to see the benefit of decoupling.

1
Task
Spring Core, level 17, lesson 1
Locked
Two forms of one fact: ApplicationEvent and payload
Two forms of one fact: ApplicationEvent and payload
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION