Blog
Storing the Story, Not Just the State: Event Sourcing in Practice
Event sourcing persists every change as an immutable event and rebuilds current state by replaying them, giving you a complete audit trail and the freedom to reinterpret history.
In a normal MERN application, your database stores current state: a user document holds the latest email, a balance field holds the current amount. When something changes, you overwrite the old value and the previous reality is gone forever. Event sourcing makes a radical choice: instead of storing current state, you store the full sequence of events that produced it. The balance is not a field; it is the result of replaying every deposit and withdrawal. Current state becomes a derived value you compute from history, not a thing you mutate in place. This sounds heavier, and it is, but it buys you a perfect audit log, the ability to reconstruct state at any past moment, and the freedom to build entirely new read models from the same history. Let us build the mental model and the practical mechanics of an event-sourced system.
State As A Fold Over Events
The core equation of event sourcing is that current state equals the left-fold of all events for an entity. You start from an empty initial state and apply each event in order, with each event transforming the state into the next version. An account starts at zero, a deposited event of one hundred takes it to one hundred, a withdrawn event of thirty takes it to seventy. The seventy is never stored as a primary fact; it is computed. This is exactly the reduce operation you already know from JavaScript arrays, applied to a stream of domain events. The events are the source of truth, immutable and append-only, while any state representation is a cache of the fold up to some point. Internalizing this reframes the whole design: you are not maintaining state, you are recording history and projecting it whenever you need a current view.
Events Are Facts, Not CRUD
The quality of an event-sourced system lives or dies on how you model events, and the most common mistake is recording CRUD operations instead of business facts. An event named user-updated with a bag of changed fields tells you almost nothing about intent and ages terribly. Events like email-changed, subscription-cancelled, or shipping-address-corrected capture why the change happened, which is the whole point of keeping history. Each event should be named in the past tense, represent a single meaningful business occurrence, and carry the data relevant to that occurrence. Because events are immutable and permanent, you design them with care, much like a public API, since you will be reading them years later and possibly building new features on top of them. Good event modeling comes from understanding the domain, which is why event sourcing pairs so naturally with domain-driven design and its focus on meaningful language.
The Append-Only Event Store
Event-sourced data lives in an event store, an append-only log of events grouped by the entity, or aggregate, they belong to. You never update or delete events; you only append new ones. To load an aggregate, you read its event stream from the beginning and fold it into current state. Kafka is a natural fit because it already is an append-only, partitioned, durable log, though purpose-built event stores also exist with richer querying. Keying events by aggregate id ensures all events for one entity land in the same partition in order, which preserves the sequence your fold depends on. The append-only constraint is liberating operationally: writes are simple appends with no in-place mutation, no update locks, and a complete history that doubles as an audit trail. The tradeoff is that reading current state requires replay, which is why the next pieces, snapshots and read models, exist.
Snapshots For Performance
Replaying thousands of events every time you load an aggregate becomes slow, so event-sourced systems use snapshots. A snapshot is a periodic saved copy of an aggregate's computed state at a specific event version, say every five hundred events. To rebuild current state, you load the latest snapshot and then replay only the events that occurred after it, instead of folding from the very beginning. This keeps reads fast even for long-lived entities with deep histories. The crucial property is that snapshots are an optimization, never a source of truth; you can always delete every snapshot and rebuild them from the events, because the events remain authoritative. This separation is what keeps the system trustworthy. If a snapshot is ever wrong, you discard it and recompute from history, which is impossible in a traditional system where the overwritten state was the only copy you ever had.
Pairing With CQRS
Event sourcing almost always travels with CQRS, Command Query Responsibility Segregation, which separates the write model from the read model. Commands flow into the write side, are validated against the current folded state, and produce new events appended to the store. Those events are then projected into read models optimized for querying, which can be ordinary denormalized tables, search indexes, or caches. The write side never serves complex queries, and the read side never appends events. This separation is what makes event sourcing practical, because folding events is great for one aggregate but terrible for queries like list all overdue invoices. You build a projection for that query instead. For a MERN developer, picture your event log as the write side and a set of purpose-built MongoDB collections or Postgres views as read models you populate by consuming events, each shaped exactly for the screens it serves.
Projections And Rebuilding Views
A projection is a consumer that reads the event stream and builds a read model, updating it as new events arrive. The superpower of event sourcing reveals itself here: because the events are the durable truth, you can create a brand-new projection at any time and rebuild it by replaying history from the start. Need a new analytics view that nobody anticipated when the system launched? Write a projection, replay all events, and it materializes complete with years of data, no migration required. You can also fix a buggy projection by deleting its read model and rebuilding it cleanly from events. This is impossible in a state-storing system, where data not previously captured is simply gone. The discipline required is that projections must be idempotent and able to replay safely, since you will rebuild them, and they must track which event position they have processed.
Versioning Events Over Time
Because events are immutable and you keep them forever, schema evolution is unavoidable and must be planned. Years from now you will still be replaying events written under an old shape, so you cannot simply change the structure. Several strategies coexist. You can version event types and have your code handle multiple versions during the fold. You can use an upcaster, a function that transforms an old event format into the new one as it is read, keeping your domain logic clean. You can favor additive, backward-compatible changes that older code tolerates. What you must never do is rewrite history to fit new code, because that destroys the audit guarantee and risks corrupting state that other projections depend on. Treat event schemas with the long-term seriousness of a database migration policy, knowing that today's events will be read by code that does not yet exist.
When The Tradeoff Pays Off
Event sourcing is powerful but genuinely complex, and it is the wrong default for most applications. It demands careful event modeling, snapshotting, projections, schema versioning, and a team comfortable with eventual consistency between write and read sides. For a simple CRUD app, it is heavy overhead with little payoff. It earns its cost in domains where history is the product: financial ledgers, where the audit trail is legally required; systems needing temporal queries like what did this look like last quarter; collaborative or undo-heavy applications; and domains where you expect to derive new views from old data repeatedly. A pragmatic approach is to apply event sourcing to the few aggregates where these benefits matter, such as the billing or ordering core, while keeping the rest of the system conventional. Used surgically on the right problem, it turns your data layer from a lossy snapshot into a complete, replayable history.