Stop storing what is. Store everything that happened, and derive what is.
A normal database stores the current answer and throws away the question. Event Sourcing inverts that: the only source of truth is an ordered, append-only log of facts that already happened — and current state is a derived value, computed by replaying that log.
The shift is small in code and large in consequence. UPDATE accounts SET balance = 300 destroys information: you now know the balance and nothing about how it got there. Appending MoneyWithdrawn(200) instead keeps the balance obtainable and keeps the history, the cause, the ordering, and the timestamp. Nothing is ever mutated; nothing is ever deleted.
state = events.reduce(apply, initialState)
Current state is a fold over history. If you can write that fold, you can throw the state away and get it back — which is exactly why this pattern buys you time travel for free.
An immutable fact in the past tense — OrderPlaced, ItemShipped. It carries what happened, not what to do. Past tense is a design rule, not style: a fact cannot be rejected or retried.
The append-only log itself, ordered per stream with a monotonic sequence number. Writes only ever append. The sequence number is what gives you optimistic concurrency and deterministic replay.
The consistency boundary that validates a command against its own replayed state, then emits events. Commands can fail; events cannot. This is where business rules live.
A read model built by consuming the stream — a table, cache, or search index shaped for one query. Disposable by design: delete it, replay, and it rebuilds. Add a new one whenever a new question appears.
A fifth piece is operational rather than conceptual: the snapshot. Replaying 4 million events to answer one balance query is not viable, so you periodically persist "state as of sequence N" and replay only the tail after it. Snapshots are a cache — they must always be re-derivable from the log, never authoritative.
MoneyWithdrawn(200). If not, reject the command — and write no event at all.# the entire read path, conceptually events = store.read("account-A", from=snapshot.seq) state = snapshot.state for e in events: state = apply(state, e) # pure function, no I/O # apply is a total function over event types def apply(s, e): if e.type == "MoneyDeposited": return s + e.amount if e.type == "MoneyWithdrawn": return s - e.amount return s # unknown event: ignore, don't crash
git checkout <sha> is temporal query, and reflog is the log outliving the state.status column loses the story the business needs.Treating events as a schema you own. Events are immutable and permanent, which means a v1 event you wrote two years ago must still be readable by today's code — forever. You cannot migrate the past. Plan for versioned events and upcasting from day one.
And: applying it everywhere. Event sourcing costs you eventual consistency between write and read side, harder queries, replay tooling, and real schema-evolution discipline. Use it where history is part of the domain — money, ownership, compliance, workflow. For a settings page, a plain row is the right answer, and reaching for this pattern is how teams manufacture two years of accidental complexity.
Say the answers out loud. If one is fuzzy, that is the sentence to reread — not the whole card.
apply() after snapshots were written.