← MODELING TRUTH

Domain Events Are Not CRUD

Storage verbs force interpretation where the log should speak plainly

Every tutorial on event sourcing eventually shows CustomerCreated, OrderUpdated, ProductDeleted. The examples look domain-shaped. They are not. Create, read, update, delete is the vocabulary of persistence — how rows in a store change — not the vocabulary of what happened in the real world.

That confusion is so common it feels normal. It is one of the quiet anti-patterns of software engineering: CRUD-ism — naming and modelling events as if the database operation were the fact. See CRUD as domain language.


Two languages that look alike

LanguageQuestion it answersTypical verbs
DomainWhat occurred in the world we care about?onboarded, converted, shipped, retracted, merged, attested
CRUD / persistenceHow did we change projected stored state?created, updated, deleted, read

The event log is append-only — facts are committed, not updated or deleted in place. CRUD is not wrong at the projection boundary: read models and current-state tables are inserted, updated, and removed; projection storage adapters translate domain events into that derived state. The failure begins when CRUD verbs become event names — when the log records that a row changed instead of what the organisation did or observed.

A CustomerCreated event tells you a new aggregate id exists in the database. It does not tell you whether the customer onboarded through self-service, was imported from a legacy system, or was a prospect converted after a sales process. Those are different facts, different authorities, different stories. One CRUD name collapses them into an undifferentiated insert — and every consumer must interpret what Created really meant.


What CRUD loses

Clarity and interpretability. The event log should be readable without decoding conventions. CustomerUpdated says something changed — not what, not why, not which process applies. Consumers infer from payload diffs, optional reason fields, or tribal knowledge. That is interpretive labour the name should have done. Audit suffers; so does everyday operations, onboarding, and any human or AI agent reading the stream cold.

Automation. Downstream systems branch on what happened, not on the fact that a column differs from yesterday.

AddressCorrected is not CustomerRelocated. One fixes a typo on the address already on file. The other records that the customer now lives somewhere else — territory, logistics, fraud, and welcome workflows may all care. AddressUpdated cannot carry that distinction unless every publisher adds reason enums and every consumer implements the same branching. You can bolt reasons onto CRUD; the result is a fragile parallel vocabulary inside the payload while the type still says nothing. Domain event names make the semantics first-class.

Information and truth. Mutable history often arrives dressed as update: the row now shows the corrected value; the wrong value vanishes. Event-sourced systems can repeat the mistake with EntityUpdated events that overwrite meaning without supersession discipline. CRUD naming encourages thinking in current state, not what was committed when.

Capture honesty. Not every append to a log is the same epistemic act. Four Capture Modes separates how evidence entered (commit, assert, measure, formalise) from what the business describes. FileUploaded sounds like a domain event; often it is a thin rename for measure (bytes at a boundary) or commit (accepted into our corpus). CustomerCreated hides both the business verb and the capture mode.

As substrate for automation, human and AI reasoning, and audit, semantically rich domain events are more powerful precisely because they are correct at the name — not because the payload can be reverse-engineered by those who already know the schema.


Projections when you only need current state

Some consumers legitimately need only the latest address (or balance, or status) — not the full narrative of how it got there. For them, handling AddressCorrected, CustomerRelocated, and a dozen other verbs feels heavy.

That need is real. The wrong fix is to collapse tier-one events into AddressUpdated so everyone shares one vague name. The right fix is a projected stream or read model: a lossy convenience with a stable contract (“current address as we project it”), explicitly not ground truth and not a substitute for the domain log. Substrate stays rich; projections serve state-only consumers. Treating the projection as the log — or letting CRUD names stand in for both — is CRUD as domain language through the back door.


Better names for the same territory

Same aggregate, richer story:

CRUD-ish nameDomain events that preserve meaning
CustomerCreatedCustomerOnboarded, CustomerImported, ProspectConvertedToCustomer
CustomerUpdatedAddressCorrected, CustomerRelocated, CustomerComplianceHoldApplied, CustomerMergedFromDuplicate
CustomerDeletedCustomerOffboarded, CustomerRecordRetired, CustomerDataErasureRequested
OrderCreatedOrderPlaced, OrderAcceptedFromQuote, ReorderInitiated
AccountCreatedProspectAccountOpened, AccountActivated, AccountProvisionedFromMigration

The point is not longer names. It is specific verbs tied to process and authority. Two events may share a stereotype effect (something new exists in our model) but must not share a generic Created label if the business distinguishes them.


Why CRUD-ism persists

CRUD is anchored in education and tooling from the first ORM tutorial onward. REST maps to resources; resources map to tables; tables map to create/read/update/delete. Event-sourcing courses often retrofit the same nouns with past tense: Created, Updated. The internet reproduces these examples faster than teams discover their cost.

Few curricula teach Event Storming, Event Modeling, or domain language before persistence. Junior developers learn that User is an entity with CRUD operations; events become CRUD in disguise. Senior teams inherit schemas named before anyone asked what actually happened. Refactoring event names feels expensive — so CRUD fossilises in the log, and people believe it is normal.

It is not normal. It is a limitation that is artificial — chosen by naming habit, not forced by event stores or message brokers. Logs happily store ShipmentDispatched and ReturnAuthorisationGranted. Nothing requires ShipmentUpdated.


Practical discipline

  1. Name what happened in the real world, not the storage operation. If the only honest description is “we inserted a row,” the model may be missing domain vocabulary — or the thing is not yet a tier-one commit and should not be logged as one.

  2. Split lifecycle by process, not by table. Prospect and customer may share a table; the events should reflect opened, qualified, converted, not one Created.

  3. Reserve CRUD for projection storage adapters — not for the event log. Inside the boundary, speak domain and append facts. Where a read model needs mutable rows, speak rows at that adapter only, and label the projection honestly. Never confuse projection convenience with substrate.

  4. Pair domain type with capture metadata when epistemic kind matters — captureForce: commit | assert | measure | formalise. See the capture ladder map.

  5. Treat generic Updated as a smell. Prefer explicit corrections, relocations, supersessions, and retirements. Optional reason fields supplement a good name; they do not rescue EntityUpdated.


Relation to Genesis

The vocabulary this essay needs — capture vs design, tier-one evidence, capture modes — is summarised in Before You Model with links to Genesis. The Genesis series argues that decisions are atoms and that the permanent record should hold captured evidence, not silent recomputation. CRUD-as-domain-language works against that thesis at the front door: if the first events in your log are ThingCreated and ThingUpdated, you have already decided that storage shape is truth shape. The rest of the architecture fights uphill from there.

This essay is standalone — not part of the Genesis narrative — because the habit predates any one system and outlives any one series. Fix the names, and Four Capture Modes, the commitment boundary, and inference-as-fact guards become usable instead of theoretical.


Principle: Domain events name the real world. Anti-pattern: CRUD as domain language. Capture taxonomy: Four Capture Modes.