Where did the money go?
Bad: Overwrite the balance row
The row says where you are, not how you got there.
Good: Append facts
Every change is an immutable event; a mistake gets a compensating PaymentReversed.
Load the stream, fold it, decide, append
evolve applies one event to the state; decide turns a command into new events, or refuses. Both are pure, so tests read like specs.
Two devices pay ₹800 from wallet-42 at once; both load v7 with ₹1,000.
The fix
Both appends expect version 7; only the first lands, as v8. The second reloads, sees ₹200 and refuses.
0.5 s
to replay a busy wallet before every payment
Assume 10 µs per event: 50,200 events. A snapshot every 500 events cuts each load to about 4 ms.
One write model, many read models
Projections subscribe to the events and build a view per screen, SQL for “my orders”, Elasticsearch for search, Redis for counters.
Ordered, but “My orders” is empty?
- Return the new position; the read side waits for it
- Read your own aggregate from the write side
- Show it at once, marked “confirming”
- Update one critical view inline
Event-source the whole system?
Bad: Everywhere, for the audit trail
A learning curve, eventual reads, events supported forever, and costly to migrate away from.
Good: Ledgers and audit-heavy workflows
Where history is the product. Plain tables elsewhere.
Remember
Append facts with an expected version, fold them to decide, project them to read. Use it where history matters.