skip to content

Domain Events

Business facts, named in the past tense, raised by aggregates as first-class model elements. This node is about events inside the domain model and the consistency they enable across aggregates; delivery, brokers and event sourcing belong to the EDA area.

part ofSoftware design & architectureoverview, primer and where to startread it →
on this pageshow

questions

6

In Domain-Driven Design, what is a domain event, and why is it conventionally named in the past tense, like 'OrderShipped' rather than 'ShipOrder'?

level: juniorimportance: must knowfreq 70%

answer

  1. past tense = fact, not request
  2. command vs event distinction
  3. aggregate raises after validating
  4. OrderShipped not ShipOrder
  5. events can't be rejected

basics

~20 s

A domain event is a note that something meaningful already happened in the business, e.g. 'OrderShipped'. It's named in the past tense because it records a fact that occurred, not a request asking for something to happen.

solid answer

~40 s

A domain event is an immutable record of something meaningful that already happened inside the domain model - a business fact, not a request. It's named in the past tense ('OrderShipped', 'PaymentCaptured') specifically to distinguish it from a command, which is imperative/present-tense ('ShipOrder') and can be rejected. Because an aggregate only raises the event after it has already validated and applied the change, the event can't be refused by a listener - only reacted to. The tense convention keeps the model's ubiquitous language unambiguous about whether an object is a request or a fact, which matters once both live side by side in the same codebase.

go deeper

for a junior

Should recognize a domain event as 'something that already happened' and reliably use past-tense names; doesn't need to design event-driven flows yet.

for a middle

Should consistently distinguish commands from events in their own code and explain why the tense matters for readability and intent.

for a senior

Should design event granularity and payload shape deliberately, and know when raising an event is overkill versus a direct method call.

for a principal

Should set naming/granularity conventions across a codebase or team, and recognize architectural drift like generic 'Updated' events as a design smell worth fixing.

## What a domain event is A domain event is a small, **immutable** object that captures a fact: something significant has already happened inside the domain model, and the rest of the system may care about it. Structurally it usually carries three things: 1. a **name** that identifies what happened; 2. an **occurred-at timestamp**; 3. just enough **data** (typically the aggregate's identifier plus a handful of relevant fields) for a listener to know what to do next. It does not carry behavior or ask for anything to be validated — by the time it exists, validation is over. ## Why the tense: a request versus a fact The past-tense naming convention exists to keep two very different kinds of objects from being confused with each other: commands and events. | | Command | Domain event | |---|---|---| | Example | `ShipOrder`, `CancelSubscription` | `OrderShipped`, `SubscriptionCancelled` | | What it is | a request — it names an intention, is phrased as an imperative | a report of something that has already been committed to the aggregate's state and passed all invariants | | Can it be rejected | it can be rejected (the order might already be shipped, the subscription might already be cancelled) | it cannot be un-happened, and nothing downstream gets a vote on whether it should have occurred | The model already decided that when the aggregate accepted the command. Naming disciplines this distinction into the code itself: **any type ending in an imperative verb is a request, anything phrased as a completed action is a fact.** This matters because command and event types frequently coexist in the same module (an application service handling `ShipOrder`, an aggregate raising `OrderShipped` in response), and without the naming convention it becomes easy to accidentally treat a fact as if it were still negotiable, or to dispatch a request to something expecting a completed fact. ## Why model events explicitly at all The main reason to model events explicitly at all, rather than just calling further methods directly from inside the aggregate, is **decoupling and expressiveness**. If `OrderAggregate.ship()` directly called `inventoryService.releaseHold()` and `notificationService.sendShippedEmail()`, the aggregate would need compile-time knowledge of every downstream concern that cares about shipping, and testing it would require mocking all of them. By instead raising an `OrderShipped` event and letting something else subscribe to it, the aggregate stays focused purely on its own invariants, and new listeners can be added without touching the aggregate's code. This is the **open/closed principle** applied to business behavior: the shipping outcome becomes a stable extension point. ## The trade-off The trade-off is **ceremony and indirection**. Every event needs: - its own type; - a place where it's raised; - one or more listeners. That is more moving parts than a plain method call for something simple like a `total` recalculation. Chasing "what happens when an order ships" now means jumping from the aggregate to wherever listeners are registered, rather than reading top to bottom in one method. Overusing events for trivial internal bookkeeping the aggregate could just do inline is a real failure mode: it turns straightforward control flow into a scavenger hunt and makes debugging harder, without buying any real decoupling benefit because nothing external actually needs to react. ## The naming-drift failure mode A second failure mode is **naming drift**: teams that reuse a single generic event like `OrderUpdated` for every kind of change lose the very benefit past-tense, fact-based naming was meant to provide. A listener subscribed to `OrderUpdated` has to inspect the payload or a `changeType` field to figure out what actually happened, pushing the responsibility of interpreting intent out of the type system and into runtime logic — effectively turning a strongly-typed fact back into an untyped notification. The naming convention only pays off if events are also granular and intent-revealing, not just tense-correct. ## A worked example A concrete example: in an e-commerce system, an `Order` aggregate exposes a `ship(trackingNumber)` method. Internally it: 1. checks invariants (order must be Paid, items in stock); 2. transitions its own status to Shipped; 3. and only then constructs and records an `OrderShipped(orderId, trackingNumber, shippedAt)` event. Nothing calls "ShipOrder" as an event — that would be the command that triggered the method, likely from an application service handling an HTTP request or scheduled job. Once `OrderShipped` exists, in-process listeners inside the same bounded context can react — for example, decrementing a fulfillment metrics counter — without the `Order` aggregate needing to know those listeners exist. The event is the seam between "what happened to the order" and "what the rest of the model does about it".

  • How would you distinguish, just by looking at a class name in the codebase, whether it's a command or a domain event?
    Commands are named as imperative verb phrases in the present tense - ShipOrder, CancelSubscription - because they represent a request that can still be rejected. Domain events are named as completed actions in the past tense - OrderShipped, SubscriptionCancelled - because they represent something the aggregate has already committed to. If a type's name could plausibly be prefixed with 'please' it's a command; if it reads naturally after 'the system observed that' it's an event.
  • Why can't a domain event be 'rejected' by a listener, and what should a listener do if it discovers a problem while handling one?
    By the time an event exists, the aggregate has already validated the change and applied it to its own state - the fact is already true in the model. A listener that finds a downstream problem has to handle that as its own failure, log it, retry it, or compensate - it cannot undo the fact that the order shipped. This is why side effects triggered by events are typically designed to be retryable or eventually consistent rather than a second validation gate.
  • Is a domain event the same thing as a message published to a broker like Kafka or RabbitMQ?
    No - a domain event is a modeling concept inside the domain layer describing a business fact; whether and how it's transported (in-process observer, application event bus, or serialized onto a broker for other services) is a separate, infrastructure-layer concern. The same OrderShipped event object can be dispatched in-process to a listener today and later be translated into an integration event on a message broker without changing what it means inside the model.

A domain event is like a receipt printed after a purchase is already rung up - it records that the sale happened; it doesn't ask the cashier whether to make the sale.

saying these in an interview costs you the question

  • Names events with imperative verbs, e.g. 'ShipOrder' used as an event type
  • Treats a domain event as something a listener can veto or reject
  • Uses one generic 'EntityUpdated' event for every kind of change
  • Can't explain the difference between a command and an event
  • Assumes 'domain event' always implies a message broker

context

open as a page

Concretely, how does an aggregate raise a domain event without breaking encapsulation - for example, without directly calling out to an event bus from inside its business logic?

level: middleimportance: must knowfreq 65%

basics

~20 s

The aggregate keeps a private list inside itself. When its business method changes state, it also adds an event object to that list. Some outside code (like an application service) reads the list after the method returns and publishes the events - the aggregate itself never talks to any bus.

open as a page

When an aggregate raises a domain event and an in-process listener reacts to it by modifying a different aggregate, should that listener's change happen in the same database transaction as the original aggregate's change? What are the consistency implications either way?

level: seniorimportance: must knowfreq 60%

basics

~20 s

You can make the listener run in the same transaction so both aggregates change together, or let it run after the transaction commits so they change separately. Same-transaction means both always agree, but couples them tightly. After-commit means they're looser, but for a moment one might be out of date.

open as a page

Why might a domain model raise a specific event like 'CustomerAddressChanged' or 'CustomerEmailVerified' instead of a single generic 'CustomerUpdated' event carrying a diff of changed fields? What's the trade-off?

level: seniorimportance: should knowfreq 55%

basics

~20 s

Specific events tell listeners exactly what happened, so they can react directly without inspecting the data. A generic 'Updated' event is easier to add but forces every listener to figure out what actually changed by digging through the payload.

open as a page

Should a domain event's payload carry the full state of the aggregate at the time it was raised, or just the minimal data needed to describe what changed, like IDs and the specific delta? What are the trade-offs?

level: seniorimportance: should knowfreq 45%

basics

~20 s

A 'thin' event carries just the key IDs and what changed, like an order ID and new status. A 'fat' event carries the whole aggregate's data. Thin events are smaller and less likely to leak stale info; fat events save listeners an extra lookup but can go stale or expose more than needed.

open as a page

When is raising a domain event the wrong choice inside an aggregate - i.e., when should a state change just be handled with a direct method call or a straightforward invariant check instead?

level: principalimportance: nice to knowfreq 35%

basics

~20 s

If nothing outside the aggregate actually needs to react, and it's a simple internal check like 'balance can't go negative,' don't make it an event - just check it directly in the method. Events are for facts something else cares about, not for every little internal rule.

open as a page