Event Sourcing inverts the storage question that most systems never think to ask. The conventional database stores the current state, the row as it is now, and the history that produced it is discarded at every update. Event Sourcing stores the history, an append only log of everything that happened, and derives the current state by replaying it, the way a version control system stores commits rather than the latest file. The inversion buys things no state store can, the complete audit trail for free, the temporal queries that answer what was true on Tuesday, and the ability to build new models of old data. It also bills costs no state store charges, debugging through replay, schema evolution over immutable history, and the privacy machinery that erasure regulations demand. This article prices both sides honestly, the model and its mechanics, the scenarios that justify the inversion, the costs with their working mitigations, and the boundary where the pattern should remain a talk someone gave at a conference.
The model and the mechanics in one running example
The pattern's mechanics are simplest as code. An account with a balance is not a row with a number, it is a stream of events, opened, credited, debited, and the state is the fold over the stream:
class Account:
def __init__(self, stream_id):
self.stream_id = stream_id
self.balance = 0
self.status = "new"
def apply(self, event):
if event["type"] == "account.opened":
self.status = "open"
elif event["type"] == "account.credited":
self.balance += event["amount"]
elif event["type"] == "account.debited":
self.balance -= event["amount"]
def debit(self, amount):
if amount > self.balance:
raise InsufficientFunds()
return Event("account.debited", amount=amount)
The write path appends, never updates, and the read path is the replay, either on load, the straightforward fold over a short stream, or from a snapshot plus the events after it for the long ones. The command methods, debit, do not change state, they validate against the folded state and emit the event that the storage appends, and the same apply function folds the event back in. The store itself is a table of append only rows, stream identifier, sequence number, event type, payload, and the sequence number is the concurrency control, a second writer appending at the same position is rejected, which makes the optimistic concurrency of the earlier isolation discussion structural rather than configured.
What the inversion buys that state storage cannot
The audit trail is the headline, and in the domains that require it, finance, compliance, gaming, the pattern is not a preference but a requirement. The state store's audit, a shadow table of changes written in parallel, is a second system that can disagree with the first, and the event log is the primary record that cannot disagree with itself because it is the only record. The temporal query is the second purchase, the balance as of the end of the quarter, the state of the order at the moment the customer complained, answered from the log by folding to a sequence number, where the state store answers the same question from a nightly backup if it answers at all.
The third purchase is the one teams underrate, the new model of old data. The report the business invents next year, the fraud heuristic, the analytics feed, the machine learning feature, is built by replaying the history into the new projection, and the history is complete because it was never summarized away. The projections themselves multiply, the balance view for the app, the daily ledger for finance, the risk feed for compliance, each built from the same events, each rebuildable from scratch, and the projection rebuild is the operational property that makes the whole architecture evolvable, because a bug in a projection is fixed by rebuilding it, and the source of truth was never wrong.
The costs that the conference talks omit
The debugging cost arrives first, because the developer's question, why is the balance wrong, is answered by a replay rather than by a row. The fold must be stepped through, the events around the defect must be inspected, and the tooling is younger and thinner than the relational tooling the team is leaving. The mitigation is the same one the incident review teaches, the replay in a test harness with the printed event stream is a precise debugger once the team learns to read it, and the event log's completeness means the defect's exact input is always available, which the state store never offered.
Schema evolution is the second cost, and it is the pattern's deepest engineering. The events are immutable and the code changes, so the 2029 version of apply must fold the 2024 events, and the discipline is upcasting, the translation of old events to new shapes at read time, plus the versioned event types for the breaking changes:
def upcast(event):
if event["type"] == "account.debited" and "amount" not in event:
event["amount"] = event["value"] # pre-2025 field name
return event
The management of upcasters is real work with a real process, deprecation windows, and a test suite that folds every historical event through every current upcaster, and the teams that skip the suite meet it again the day the oldest stream must still load. The privacy cost is the third, the erasure request against an immutable log, and the working answer is the separate PII store with events carrying identifiers rather than names, so the erasure empties the side table while the log keeps its shape, a design constraint that is cheap on day one and structural surgery on day one thousand.
Where the pattern earns its keep
The justified scenarios share a signature, the history is the business. The ledger, where every movement must be reconstructible and the audit is the product. The gaming and loyalty domain, where the play history drives both the balance and the analytics. The ordering and fulfillment flow, where the state machine over the events is the operational record. The compliance-heavy insurance and healthcare flows, where the question what did the system know when it decided is answered from the log by design rather than from a restoration exercise. In each of them the event log is not an implementation detail, it is the requirement, and the pattern's costs are the premium the domain charges for its guarantees.
The unjustified scenarios share the opposite signature, the history is disposable. The product catalog where the current state is all anyone queries. The user profile that changes without ceremony. The general CRUD surface of an ordinary application, where the append only log is a slower row store with a debugging tax, and the team that ships it there has bought the machinery of a bank for the data model of a blog. The honest assessment question is the one this article can leave the reader, would anyone pay for the answer to what was the state last Tuesday, and if the honest answer is no, the pattern is the wrong tool regardless of how good the talk was.
Testing the fold and the command guard
The test suite of an event sourced system has a shape worth learning, because it is different from the state store suite and better at the thing that matters. The unit of behavior is the decision, given these events, when this command arrives, expect these events, and the test states it exactly that way:
def test_debit_rejected_below_balance():
account = Account.replay([
Event("account.opened"),
Event("account.credited", amount=100),
])
with pytest.raises(InsufficientFunds):
account.debit(150)
def test_debit_emits_event():
account = Account.replay([
Event("account.opened"),
Event("account.credited", amount=100),
])
event = account.debit(40)
assert event == Event("account.debited", amount=40)
account.apply(event)
assert account.balance == 60
The tests fold real event sequences and assert real decisions, no database, no container, and the fixture is a list of events that reads like the transcript of a real account. The property that makes this suite durable is the historical regression, the production log itself, any defect found in the wild becomes a test by pasting the actual events into a test and asserting the corrected decision, so the same defect can never return through the same history, which the state store world achieves only with disciplined fixture archaeology.
def test_gh_481_double_debit_fold():
account = Account.replay(production_stream("acc-81234"))
assert account.balance == 260 # was 180, the upcast missed the fee credit
The operational surface around the log
The log alone is storage, and the operational pattern around it is what makes systems run. The projections, the read models, are built by subscribing to the stream, folding events into queryable stores, and the subscription tracks its position, the sequence number it has consumed, which makes the projection resumable and the rebuild a position reset. The snapshots cut the fold cost for the long streams, the pattern's own optimization, fold every hundred events, checkpoint the state, and the snapshot is always disposable because the events remain. The event store products, the purpose built databases and the message streams that teams use, provide the subscription machinery, the versioning and the optimistic concurrency out of the box, and the plain relational event table plus the team's own subscriber is the honest starting version that many production systems never outgrow.
The observability of the whole system improves because the log is the record, and the metrics that matter are the stream lag, the projection lag and the subscription health, the same lag vocabulary the replication and Kafka discussions use. The monitoring that watches the log watches the system, and the incident question, which events did the projection miss, has a mechanical answer in the position arithmetic that the state store world spends its whole incident budget guessing at.
The decision checklist before the first stream is opened
The pattern is adopted deliberately or suffered accidentally, and the checklist that decides is short:
- Confirm the history is a business requirement, audit, temporal queries or the reconstructible decision record;
- Budget the engineering for schema evolution before the second event type ships, upcasters and the historical fold test;
- Design PII out of the event payloads from the first event, identifiers only, names in the side store;
- Start with the relational event table and the team's own subscriber, the purpose built store is a scale decision;
- Snapshot the long streams and monitor the projection lag with the same dashboards that watch replication;
- Rehearse the projection rebuild quarterly, the rebuild is the capability that makes the whole design safe.
The log as the source of truth is the architecture the financial world converged on for its own reasons, and the teams whose domains share those reasons inherit decades of hard won patterns. The teams whose domains do not should keep the fold in their toolkit, it is a beautiful function, and leave the storage question their state store already answers correctly.
The temporal query paying off in one support ticket
The support ticket makes the temporal purchase concrete. A customer disputes a charge from last month, the balance shown on their statement, and the agent needs the answer to one question, what did the account look like when the charge landed. The state store answers this with archaeology, find the backup from that night, restore it somewhere isolated, query it, and hope the charge was not in the hours between backups. The event log answers it in one fold:
events = store.read_stream("acc-81234", up_to=seq_at("2026-09-12 14:32:00"))
account = Account.replay(events)
assert account.balance == 260
assert account.holds == [Hold("merchant-4471", amount=40)]
The sequence position is computed from the timestamp, the fold runs in milliseconds, and the agent's answer is exact rather than approximate, the balance at the moment of the dispute, not the balance of the nearest backup. The teams that live in regulated domains describe this as the moment the pattern's cost stopped being a debate, because the dispute process that used to take an afternoon became a query, and the query works for any past moment, which turns the audit question from an emergency procedure into a first class product feature.