Event Sourcing Is About Behaviour, Not State

event-sourcingdomain-driven-designvalue-objects

One of the more subtle difficulties with Event Sourcing is that changing the persistence mechanism does not necessarily change the way we model the domain. A system may store events instead of rows, rebuild aggregates from streams and expose domain methods rather than public setters, while still being shaped by the same state-oriented thinking that would have produced an ordinary CRUD model.

This becomes particularly visible when modelling orders. An order has an obvious representation in state: it has a customer, a delivery address, a collection of lines, a payment state and some indication of where it is in its lifecycle. When the design begins from those properties, it is natural to create events that correspond directly to them.

OrderCreated
CustomerIdChanged
DeliveryStreetChanged
DeliveryPostalCodeChanged
DeliveryCityChanged
OrderLineAdded
OrderLineQuantityChanged
OrderStatusChanged

The resulting aggregate can be rebuilt correctly, and every modification is preserved in the event stream. From a technical perspective, this is Event Sourcing. The stream is the source of truth, and the current state is derived by replaying its events.

What is less clear is whether the stream represents the domain particularly well.

An order is meaningful to the business because of what happens during its lifetime. It is placed by a customer, confirmed under certain conditions, changed before fulfilment, cancelled for a reason, shipped to a destination and eventually completed. These are not simply different combinations of property values. They are business transitions with rules, intentions and consequences.

When the events are designed around the aggregate’s properties, that meaning becomes secondary. The stream records that the street changed, then the postal code changed, then the city changed, but it does not necessarily record that the customer changed the delivery destination. It records that the status became Confirmed, but not what confirmation meant or which business operation caused it.

This is the distinction between Event Sourcing and what is sometimes called property sourcing.

Property sourcing treats the aggregate as a collection of independently changing properties. Each property tends to acquire its own method and its own event, and the model gradually becomes systematic in the same way as a data-entry form. If the aggregate contains a field, there is likely to be a corresponding Change method, and if that method is invoked, a corresponding Changed event is appended.

order.ChangeDeliveryStreet(...);
order.ChangeDeliveryPostalCode(...);
order.ChangeDeliveryCity(...);

Each method may contain validation, and the design may appear more sophisticated than exposing public setters, but the underlying abstraction remains the same. The behaviour is derived from the shape of the data rather than from the operations recognised by the domain.

A more domain-oriented model would treat the delivery destination as one concept and changing it as one operation.

order.ChangeDeliveryAddress(new DeliveryAddress(
    street,
    postalCode,
    city,
    country));

The resulting event could preserve the same concept.

public sealed record DeliveryAddressChanged(
    OrderId OrderId,
    DeliveryAddress Address);

This is not merely a matter of reducing the number of events. The important difference is that the event represents one meaningful transition. The street, postal code, city and country belong together because the domain treats them as one delivery address, and the DeliveryAddress value object makes that relationship explicit.

Value objects are particularly useful here because they encourage composition around meaning rather than decomposition around storage. Instead of sourcing several primitive properties independently, the event contains a value that is valid and meaningful as a whole. The event becomes harder to express in an invalid or partially updated state because its data reflects the same concept as the domain operation.

With one event per property, an address change may temporarily be represented as several unrelated facts.

DeliveryStreetChanged
DeliveryPostalCodeChanged
DeliveryCityChanged

This raises questions that exist only because the event model has been decomposed according to properties. Was this one address change or three separate changes? Can projections observe the intermediate state? What happens if the first two events are appended but the third is not? Does changing the postal code without changing the city represent a valid business operation?

A composed event avoids those accidental questions because it preserves the transition at the level at which it occurred.

DeliveryAddressChanged

The event may contain several values, but it still represents one fact. Having several properties in an event is therefore not a failure of event design. In many cases, it is evidence that the event captures a richer domain concept rather than exposing every primitive field independently.

The same distinction appears throughout the order lifecycle. An order may contain several properties related to cancellation, such as a status, cancellation reason, cancellation timestamp and the identity of the person who cancelled it. Property sourcing encourages each of these to be treated as a separate modification.

OrderStatusChanged
CancellationReasonChanged
CancelledAtChanged
CancelledByChanged

A meaningful domain event composes them into the business transition they collectively describe.

public sealed record OrderCancelled(
    OrderId OrderId,
    CancellationReason Reason,
    CancelledBy CancelledBy,
    DateTimeOffset CancelledAt);

The event contains more properties than any of the property-sourced alternatives, but it is also more cohesive. Every value contributes to the same fact, and removing one of them would change what the event means or weaken the historical information it preserves.

This is where the number of properties in an event can become a misleading measure. A large event is not automatically a snapshot, just as a small event is not automatically a good domain event. An event with one primitive value may be pure property sourcing, while an event composed from several value objects may express one precise and important domain transition.

The relevant question is not how many fields the event contains, but whether those fields belong together as part of the same fact.

There is also another state-oriented design that should be distinguished from property sourcing. Instead of creating one event per property, a system may use broad events that resemble persisted CRUD operations.

public sealed record OrderUpdated(
    Guid OrderId,
    Guid CustomerId,
    string Street,
    string PostalCode,
    string City,
    IReadOnlyList<OrderLineDto> Lines,
    string Status);

This could be described as CRUD sourcing. Rather than storing the latest row directly, the system stores successive updates or snapshots from which the latest row can be reconstructed. The event says that the order was updated, but the meaning of the update is hidden inside a collection of changed values.

Property sourcing and CRUD sourcing look different, but both remain centred on state.

Property sourcing decomposes state too far, producing one event for each field or mutation.

DeliveryStreetChanged
DeliveryPostalCodeChanged
OrderStatusChanged

CRUD sourcing keeps the state together, but without preserving the business transition that changed it.

OrderUpdated

Event Sourcing, when used as a domain modelling approach, is concerned with something else.

OrderPlaced
DeliveryAddressChanged
OrderConfirmed
OrderCancelled
OrderShipped

These events may change one property or many properties when they are applied. That is largely an implementation detail. Their purpose is to preserve the operations and outcomes that matter in the domain.

An OrderConfirmed event may set the order status, record the confirmation time and prevent further changes to certain parts of the order. An OrderCancelled event may alter the status, release reserved stock, preserve the cancellation reason and make the order ineligible for fulfilment. The state transition can be broad even when the event describes one coherent fact.

This is one reason status-change events are often weak. A status usually summarises the result of behaviour that has already occurred.

OrderStatusChanged

This tells us how the representation changed, but not why. The same transition to Cancelled could result from a customer request, failed payment, fraud detection, expired reservation or an administrative correction. Those may have different rules and different consequences even if they happen to produce the same status value.

Events such as OrderCancelledByCustomer, OrderCancelledAfterPaymentFailure or a more general OrderCancelled containing a meaningful CancellationReason preserve information that OrderStatusChanged loses. The status can still be derived when the event is applied, but it no longer defines the event itself.

The aggregate methods tend to improve at the same time because they are no longer organised as setters. Instead of exposing ChangeStatus, ChangeCancellationReason and ChangeCancelledAt, the aggregate exposes the operation that the business is trying to perform.

order.Cancel(reason, cancelledBy, cancelledAt);

The aggregate can then evaluate the rules surrounding cancellation as one decision. It may reject cancellation after shipment, require a reason for certain order types or handle already captured payment differently from an unpaid order. Those rules belong to the operation as a whole and become harder to see when the behaviour has been distributed across individual property mutations.

This does not mean that every change in an order must be elevated into a profound business event. Some information may genuinely behave like editable reference data, and an event such as CustomerReferenceChanged may be entirely appropriate when that is how the business understands the operation. The point is not to avoid events ending in Changed, nor to manufacture elaborate names for simple behaviour. The point is to derive the event from the domain operation rather than mechanically deriving it from the aggregate’s fields.

The same restraint applies to the boundary of Event Sourcing itself. Once a system begins preserving domain events, there is often a temptation to represent every activity as another event in the aggregate stream. An order may be exported to an ERP system, sent to a warehouse, rendered as a PDF or included in an email. These operations may be important to the reliability of the application, but they do not automatically become part of the order’s domain history.

Suppose the system confirms an order and then transfers it to an external fulfilment platform. The confirmation has meaning in the domain. It may establish an agreement, lock prices, reserve stock and make the order eligible for fulfilment. The transfer exists because another system needs to know about that decision.

Recording OrderTransferredToFulfilmentSystem in the order stream may be appropriate when the business itself recognises successful handover as part of the order lifecycle. In other systems, the transfer is entirely technical. It may be retried several times, routed through different transports or replaced by another integration without changing what an order means.

In that case, the transfer state belongs elsewhere. It might be stored in an outbox, an inbox, a process manager, an integration database or an ordinary relational table. The fact that the surrounding application uses Event Sourcing does not require every concern to be event sourced.

This is another useful distinction. Event Sourcing is not the same as storing everything as an event. It is a way of preserving the history of a domain model where that history has value. An application may contain event-sourced aggregates alongside conventional state-based models, transient workflows and technical persistence mechanisms. Consistency of technology is less important than clarity of responsibility.

Orders are a good example precisely because their history often has domain value. Knowing that an order was placed, amended, confirmed, cancelled or shipped can explain its current state, support later decisions and preserve facts that matter to the business. The retry count of an HTTP request or the timestamp of a generated PDF usually belongs to a different kind of history.

A meaningful order stream might therefore look like this:

OrderPlaced
OrderLineAdded
DeliveryAddressChanged
PaymentAuthorised
OrderConfirmed
OrderShipped

A property-sourced stream might preserve the same final state in a much more fragmented form:

OrderStatusChanged
OrderLineCollectionChanged
DeliveryStreetChanged
DeliveryPostalCodeChanged
DeliveryCityChanged
PaymentStatusChanged
OrderStatusChanged
ShipmentReferenceChanged

A CRUD-sourced stream might preserve it through a sequence of broad updates:

OrderCreated
OrderUpdated
OrderUpdated
OrderUpdated
OrderUpdated

All three approaches can technically rebuild an order. What differs is the history they preserve.

The property-sourced stream describes individual mutations to the model. The CRUD-sourced stream describes successive versions of its state. The domain-event stream describes the lifecycle of something meaningful to the business.

That distinction is where much of the value of Event Sourcing lies. Rebuilding state is necessary, but it is not sufficient. The stream should preserve the reasons the state exists in its current form, using events that reflect the operations and concepts through which the domain evolves.

Value objects support this by allowing events to carry complete domain concepts instead of collections of loosely related primitives. Aggregate methods support it by representing decisions rather than setters. Domain events support it by recording meaningful transitions rather than individual fields or generic updates.

The result is not necessarily a smaller event model, and it is not always a simpler one. It is, however, a model in which the events have a reason to exist beyond the mechanics of reconstructing an object. They form a history that remains understandable even when the internal shape of the aggregate changes.

That is the difference between sourcing an order, sourcing its properties and sourcing the CRUD operations performed against it. All three can use an event store, but only one begins with the behaviour of the domain.