Before You Reach for Event Sourcing
CQRS and Event Sourcing are two separate decisions that get adopted as one bundle, usually because someone said the word "audit." Each solves a specific problem and charges a permanent complexity premium. Here is what each actually buys, and the cheaper answer when the real requirement was history.
"We chose event sourcing because we needed an audit log."
An append-only history table would have taken a day. The team spent a quarter, and now every developer who joins has to learn how to answer what is this customer's current balance by replaying a log.
The patterns are not the problem. The bundling is.
Two decisions, not one
They arrive as a package — "CQRS/ES" — and they are separable. Most teams saying it mean one of them.
CQRS splits the model in two. A single domain model optimised for writes — aggregates guarding invariants — is often a poor fit for reads, which want denormalised, cross-aggregate views. Forcing one model to serve both produces awkward repository queries, lazy-loading gymnastics, and aggregates bloated to satisfy a screen. So: a command side that mutates through aggregates, a query side that is a separate read-optimised projection.
Event Sourcing changes what is stored. Instead of a row holding current state, you persist the ordered sequence of events that happened to the aggregate. To load it you replay; to save you append. The event log becomes the system of record.
You can do CQRS without event sourcing — a read model projected from a perfectly normal relational write model is extremely common and much cheaper. You can technically do ES without CQRS, though the read pain usually drags you into CQRS eventually.
Knowing which one you are proposing is most of the decision.
What Event Sourcing genuinely buys
Storing only current state throws away history. You cannot answer "how did it get this way?", audit is bolted on afterwards, and for domains where the sequence of changes is the business truth — finance, logistics — current-state storage is lossy about the thing that matters.
What you get back:
- The past as a first-class fact, not a reconstruction from logs.
- Temporal queries. What did this look like on the 3rd? Not a special report — an ordinary replay.
- Projections you did not know you needed. New read model six months later, built from history that was already captured. This is the genuinely magical one.
- Correction as a domain operation. A compensating event, rather than an UPDATE that erases evidence.
That last point is the tell for whether you belong here. In a ledger, "we got it wrong" is a business event with its own record. In a CRUD app, it is a bug fix.
The premium, itemised
The cost is permanent and lands on people who were not in the room:
Event versioning, forever. Events are immutable and you must still be able to replay the ones you wrote in year one after the schema has moved on three times. Upcasting, versioned event types, tolerant readers — this is the tax nobody costs out in advance, and it never goes away.
Replay as a maintained capability. Not a party trick — a thing that must work, be fast enough, and be tested. Which means snapshots once aggregates get long-lived, and snapshot invalidation when projections change.
A read model that is always eventually consistent. One transaction modifies one aggregate; everything outside that boundary catches up afterwards. Write then immediately read, and the user may not see their own change. Every screen must now answer "what if the projection lags?"
Onboarding. A new engineer cannot open a table and see the state. This is a real, recurring, permanent cost, and it is the one most consistently underweighted.
Cheaper answers to the actual requirement
Nine times in ten the stated requirement is "we need to know what happened." Options, in ascending cost:
- An append-only audit table. Who, what, when, before, after. A day's work. Answers the compliance question completely.
- Change data capture. The database already has a log; stream it. No application change at all.
- Domain events published for integration, while state remains the record. You get the event stream other services need without making events your source of truth.
- CQRS alone. A read model shaped for reading, projected from ordinary state. Solves the "our queries are awkward" problem, which is often what was actually meant.
- Event sourcing. When the history is the product.
Most teams reaching for (5) had the requirement for (1).
When it genuinely fits
Not a hedge — the real cases, because they exist:
- The domain is intrinsically about events. Ledgers, trading, insurance claims, shipment tracking. The events are not an implementation detail; they are the vocabulary the business already speaks.
- History is the product. Users ask temporal questions as a feature, not an audit obligation.
- Correcting the past is a business operation with its own rules and approvals, rather than an incident.
- You need projections you cannot yet name, and the cost of not having captured the history is high.
If two or more of those hold, the premium is probably worth it and the alternatives will fight you.
The one-sentence test
Before any pattern name is spoken, write the requirement down with no pattern names in it.
"Compliance must see who changed a payout and when."
That is an audit table.
"Support must reconstruct the account as it stood at any past date, and corrections must themselves be auditable."
That is closer to event sourcing.
If the sentence cannot be written without saying "event sourcing," the requirement has not been found yet — the solution has just been chosen early and is now recruiting justifications.
The takeaway
Event Sourcing is powerful and occasionally the only honest model of a domain. It is also a permanent tax on everyone who touches the system afterwards, paid in versioning, consistency, and onboarding.
Name the requirement without naming a pattern. Then pick the cheapest thing that satisfies it.
Quality is a requirements decision, and so is complexity. Spend it where the domain actually demands it.