Modeling Facts and Reactions with Domain Events

deniskyashif1 pts1 comments

Modeling Facts and Reactions with Domain Events · Denis Kyashif's Blog

July 25, 2026

Edit

software-architecture

domain-driven-design

Modeling Facts and Reactions with Domain Events

This article is a part of an ongoing series on Domain-Driven Design.<br>You can check out the rest of the series here.

Most business logic eventually runs into the same shape:

When something happens, something else should happen.

In this article we look at domain events : a way to record what happened, and let each consequence react to it independently, without the fact having to know about any of them.

The problem

Consider this simple workflow.

placeOrder(...)<br>save(order)<br>sendConfirmationEmailToClient(order)

For a simple application, defining this in a transaction script makes sense. Zoom in a little, however, and the code tells another story. The important domain fact is that an order was placed. Sending a confirmation email is a consequence of that fact, not part of the fact itself.

Coupling facts and consequences creates several potential issues with our design. Let&rsquo;s update our requirements:

We want placing the order to also notify our fulfillment department.

We want this order to be recorded in analytics.

placeOrder(...)<br>save(order)<br>sendConfirmationEmailToClient(order)<br>notifyFulfillment(order)<br>updateAnalytics(order)

Now we have an ordered list of unrelated work that grows as our system gets more complex. It obscures the domain fact that an order was placed and makes it harder to see which consequences are essential.

It also couples the customer-facing request to external systems. The order may be saved successfully while email delivery, fulfillment notification, or analytics fails. These operations cannot usually be made atomic with the order write, despite being grouped in the same script. Reporting the whole operation as failed can mislead the customer into placing a duplicate order, whereas reporting it as successful can leave follow-up work unfinished. Each consequence needs its own error handling, retries, and fault tolerance.

There is a subtler problem too. Placing an order may not happen in just one place, so we have to remember to include the same set of subsequent actions at every call site.

There must be a better way to handle this. To get there, we must first ask:

What happened, and who needs to know?

Then record the domain fact in one place, allowing each consequence to react to it independently.

Domain events describe facts

An order being placed is a fact in the domain. A domain event records such a fact in the language the business uses. It is not an instruction to perform work: SendConfirmationEmail describes a technical task, while OrderPlaced describes something that has already happened.

Events are named in the past tense because they are historical records. A handler cannot reject OrderPlaced; it can only decide how to react. For the same reason, an event should be immutable . If the order later changes, that is a new fact and may warrant a new event rather than a revision of the old one.

An event needs enough information to identify and understand the fact. That usually includes the affected entity, relevant values at the time, and when the event occurred. The exact payload is a design choice: a handler may use identifiers to query a read model, while a self-contained event may carry a snapshot of the values handlers need. Avoid exposing the aggregate&rsquo;s internal representation merely for convenience. As a rule of thumb, prefer lean events, unless you have a clear justification to do otherwise.

OrderPlaced<br>orderId<br>customerId<br>items<br>placedAt

This is also what distinguishes an event from a command . A command expresses intent: PlaceOrder asks the model to do something and can be refused. An event records the outcome: once the model accepts that command, OrderPlaced records the decision. The event is raised in memory at that point, the fact becomes durable when the transaction commits, and the event can then be dispatched to interested handlers.

Raise events where the decision is made

The place to record a fact is where the fact becomes true. In a rich domain model, that is the aggregate or domain operation that enforces the relevant rules. OrderPlaced does not mean merely that a row was inserted. It means the model accepted the request and satisfied its invariants, so the event belongs to that decision.

The sequence is: validate the rules, change the state, then record the event. The aggregate keeps the event alongside its state until the unit of work collects it.

class Order:<br>events = []

static place(customerId, items):<br>if items is empty: reject<br>order = new Order(customerId, items)<br>order.events.add(OrderPlaced(order.id, customerId, items.ids, now))<br>return order

Nothing outside the aggregate constructs OrderPlaced. The application service asks the model to do the work and persists the result:

order = Order.place(customerId,...

order domain fact event events orderplaced

Related Articles