Architecture and Scale

Event Sourcing

Instead of storing current state, you store the sequence of events that produced it. Event sourcing gives you a full audit log and time travel, at the cost of complexity.


The Concept Explained

A conventional database stores current state. A row holds an account balance of 340, and when money moves the row is updated to 290. The previous value is gone, and so is any record of why it changed.

Event sourcing stores the changes instead. Rather than a balance, the store holds AccountOpened, MoneyDeposited, MoneyWithdrawn, in order, appended and never modified. Current state is not stored at all: it is derived by replaying the events from the beginning.

The accounting analogy is exact and worth using in an interview. A ledger does not erase entries when a balance changes; it appends a new entry, and the balance is the sum. Erasing entries would be unthinkable, because the entries are the record and the balance is merely a summary of them. Event sourcing applies that discipline to application state.

KEY CONCEPT

The inversion is which one is authoritative. Conventionally, state is the truth and any event log is a side effect for auditing. In event sourcing the events are the truth and state is a derived cache that can be discarded and rebuilt. That is why history cannot be lost: it is not a record of what happened to the data, it is the data.


How It Works

Writes append events to a stream, typically one stream per entity. Reading an entity means loading its events and folding them into current state. Nothing is ever updated in place.

Reading, Writing, and Keeping Replay Affordable

Click each step to explore

What You Genuinely Gain

Complete history, for free and by construction. Not an audit log someone remembered to write, but the actual mechanism of storage. Every change, its cause, its order. For regulated domains this alone can justify the pattern.

Temporal queries. What was this account's balance last March? Fold the events up to that date. Conventional systems cannot answer this unless someone anticipated the question and stored history explicitly.

Debugging by replay. A corrupted state can be investigated by replaying the events that produced it, and a bug in the fold logic can be fixed and the state regenerated correctly. Conventional systems lose the information needed to do this.

New read models over old data. Add a projection today and run it over all historical events to populate it fully. Conventionally a new view only accumulates data from the moment it is deployed.

Appends do not contend. No update locks on hot rows, since every write is an append.

What Makes It Hard

Schema evolution, which is the hardest part. Events are immutable and permanent, so an event written three years ago in an old shape must still be readable today. You cannot migrate them the way you would alter a table, because rewriting history destroys the property the pattern exists for.

The standard approaches are versioned event types with the ability to read every version, upcasting old events into current shapes on read, or weak schemas that tolerate missing fields. All of them mean the codebase permanently carries knowledge of every event shape it has ever written. That burden only accumulates.

Querying is genuinely bad. "Find all accounts with a balance over 1000" is a question the event store cannot answer without replaying every stream. This is why event sourcing almost always requires CQRS: the write side stores events and read models exist to answer queries. Treating them as separate choices is correct in principle, and in practice event sourcing without a read model is close to unusable.

Deleting data conflicts with immutability. A right-to-erasure request meets an append-only store that is not supposed to be modified. Approaches include crypto-shredding, where the personal data is encrypted and the key is destroyed, or accepting that some streams must be rewritten. Neither is comfortable, and this needs deciding before launch rather than after.

Getting the event granularity wrong is expensive. Too coarse, and events carry little meaning beyond "something changed," losing the benefit. Too fine, and streams grow enormous. Unlike a schema, you cannot easily revise this later, because the old events persist in whatever granularity you chose.

WARNING

Events must record what happened in business terms, not what changed in the database. AddressCorrected and CustomerMoved produce the same field update and mean entirely different things, and a projection distinguishing them can only exist if the events did. An event stream of CustomerUpdated payloads is a change log wearing event sourcing's clothes, and it forfeits most of the benefit while paying the entire cost.


System Design Implications

Use it where history is part of the domain. Financial ledgers, order lifecycles, inventory movements, insurance claims, anything regulated or audited. In these domains you were going to build history tracking anyway, and event sourcing does it correctly by construction rather than as a bolt-on.

Do not use it for CRUD. A user profile service storing names and preferences gains almost nothing and takes on schema evolution, snapshotting, and query difficulty. This is the most common misapplication.

Apply it per aggregate, not per system. Event-source the order lifecycle and the ledger; keep the product catalogue and user preferences conventional. Mixed persistence within one system is normal and correct, and proposing it selectively demonstrates better judgement than proposing it wholesale.

Plan snapshots from the start. Not because you need them immediately, but because the naive load path becomes untenable at a stream length you can predict, and retrofitting snapshots after performance degrades is done under pressure.

Decide versioning strategy before the first event ships. Once events exist in production their shape is permanent. Choosing upcasting or versioned types early is far cheaper than deciding after three incompatible shapes are already in the store.

Pair it with CQRS. The event store answers "what happened to this entity" and nothing else well. Read models answer everything else. Design them together.

Be careful with external side effects during replay. Rebuilding state by replaying events must not resend emails or recharge cards. The fold that computes state must be pure, with side effects living in projections that know whether they are catching up or live.

PRO TIP

For the hard-parts half of the question, lead with schema evolution because it is the answer that shows real experience. Events are immutable and permanent, so a three-year-old event in an old shape must still be readable, and the code carries every version it has ever written forever. Then add that querying is poor enough to make CQRS effectively mandatory, and that deletion conflicts with immutability in a way regulated domains have to resolve deliberately.


Tradeoffs and Decision Framework

DimensionState-based storageEvent sourcing
HistoryLost unless addedInherent and complete
Temporal queriesNot possible retroactivelyNatural
Querying current stateDirectRequires read models
Write contentionRow locksAppends, no contention
Schema changeMigrate the tablePermanent multi-version support
Storage volumeCurrent state onlyEverything, forever
Deleting dataStraightforwardConflicts with immutability
Load costOne readReplay, or snapshot plus replay

The framework in four questions. Is history genuinely part of the domain, or would you be building an audit log anyway? Can the team carry permanent event versioning, since that burden never reduces? Will CQRS be in place, because the query story depends on it? And is there an answer for data deletion that satisfies whatever regulation applies?

Four yeses in a domain where history matters makes event sourcing a strong choice that pays back for years. Applied to CRUD it is one of the most reliable ways to make a simple system permanently complicated, and recognizing that boundary is the substance of the answer.


Common Mistakes

Using it for CRUD. No historical requirement means all cost and no benefit.

Events named after database changes. CustomerUpdated loses the business meaning that made the events worth storing.

No versioning strategy. Events are permanent, so the first breaking shape change becomes a crisis rather than a routine migration.

No snapshots. Replay cost grows without bound and the load path degrades at a size you could have predicted.

Assuming the event store can answer queries. It answers per-entity history. Everything else needs a projection.

Side effects inside replay. Rebuilding state must not resend notifications. The fold must be pure.

Event-sourcing the whole system. It belongs on the aggregates whose history matters, not everywhere.

No plan for deletion. Append-only storage and a right to erasure collide, and the resolution needs designing up front.


INTERVIEW QUESTION

Explain event sourcing. What do you gain by storing events instead of current state, and what are the hard parts (like schema changes and rebuilding state)?