Derive, don't store: a ledger you cannot corrupt
Event sourcing discipline without an event-sourcing framework — what it buys, what it costs, and the honest test for whether your domain actually needs it.
4 min read Event sourcing, Domain modelling
“Event sourcing” usually arrives attached to a framework, a projection engine, an event store and a two-week ramp-up. That packaging has put a lot of teams off a good idea, because the idea itself is much smaller than its usual delivery vehicle.
The idea is one rule:
Store facts. Derive everything else. Never store a value you could recompute.
You can adopt that rule in an ordinary relational schema, with ordinary models, on a Tuesday. No event store required. What follows is what it bought us in a financial ledger, and what it cost.
The rule, concretely
In a portfolio tracker there are exactly two kinds of fact:
- Capital events — money entering or leaving: deposits, withdrawals, dividends.
- Trade events — a signed quantity of an instrument at a price, plus fees and taxes.
That is the whole write model. Two append-only tables.
Everything a user actually looks at is absent from the schema: current position, weighted average cost, cash balance, realised gain, unrealised gain, total equity, allocation, tax liability. All of it is computed by replaying the log.
Corrections are new events, never edits. A trade entered wrong three months ago is fixed by recording a correcting event, not by updating the original row. The log is what happened, including the mistakes.
What it makes impossible
This is the part worth being precise about, because the benefit is not “cleaner code” — it is a category of bug that stops existing.
Drift. When a balance is stored, it is a second source of truth. Every code path that writes an event must also update it, correctly, under concurrency, forever. Miss one — a bulk import, an admin correction, a rollback — and the stored balance and the log disagree. There is no drift when there is nothing to drift from.
Migration corruption. Backfilling a stored aggregate is a one-shot operation you have to get right against production data. Derived values need no backfill; change the function and every answer changes with it.
Untraceable numbers. Every figure on screen has an origin you can walk back to specific rows. “Why is my average cost 43.20?” is answerable, exactly, every time. In a stored-balance design that question frequently has no answer at all.
What falls out for free
A good sign you picked the right model is that features you did not design for turn out to be already built.
Time travel. “What did this look like in March?” needs no snapshot table. It is the same
computation with the event stream filtered by date — a where clause, not a feature.
Inferred properties. How much fresh capital went in versus how much came from trading gains is derivable from the log alone. Nothing needed tagging at entry time, which matters enormously because users do not reliably tag anything.
Audit for free. The write model is the audit log. There is no separate table recording what changed, because nothing ever changes.
What it costs
Recompute latency, on every read. This is the whole bill, and it is real. You pay it with caching — which becomes the actual hard problem of the design, and which we wrote about separately in two-dimensional cache versioning .
Replay order becomes load-bearing. Realised gain depends on the sequence sells happened in.
Events need a reliable ordering that is not “whatever id came out as”, and backdated entries have
to slot into the right position rather than the end.
Discipline that the schema does not enforce. Nothing stops a future contributor adding a
current_balance column for a quick dashboard win. We wrote the rule down as a short
non-negotiable document in the repository and tested the invariants directly — cannot oversell,
cash must reconcile exactly. A rule that lives only in someone’s head is not a rule.
Some things genuinely are facts. A closing price on a given day is not derivable from anything; it is an observation and it gets stored. The rule is “derive what is derivable”, not “store nothing” — and confusing the two leads to some very silly conversations.
The test
Ask one question about the aggregate you are about to store:
If this value and the records it summarises ever disagreed, how would you find out?
If the answer is “a customer would tell us”, or “we would not”, you are looking at a value that should be derived rather than stored. If the answer is “it does not matter much either way” — store it, and get on with something more interesting.