Event Sourcing Explained: Store Changes, Not State
Event sourcing stores every state change as an immutable event instead of overwriting current state. How it works, and when CQRS pairs with it.
Event sourcing is an architectural pattern where every change to an application’s state is stored as an immutable, append-only event, instead of overwriting a row in a database. Rather than a bank_accounts table holding a single current balance column, an event-sourced system stores a log — AccountOpened, FundsDeposited, FundsWithdrawn — and derives the current balance by replaying that log from the beginning.
How it differs from traditional CRUD
A conventional CRUD application keeps the current state and discards the path that got there. Update a customer’s address, and the old address is gone unless something explicitly logs it. Event sourcing inverts this: the events are the source of truth, and current state is just one possible view computed from them.
This has a direct consequence for auditability. In a CRUD system, “what was this order’s status at 3pm yesterday” requires a separate audit log bolted on after the fact — if one exists at all. In an event-sourced system, that question is answered by replaying events up to that timestamp, because the full history was never discarded in the first place.
The event store
The event store is an append-only log, conceptually similar to a write-ahead log or a Kafka topic — see what a message queue is and Kafka vs RabbitMQ for related log-based infrastructure. Each event is immutable once written: you never update or delete an event, you only ever append a new one. If a withdrawal was recorded in error, the correction is itself a new event — WithdrawalReversed — not an edit to the original record.
Events are typically stored per aggregate (a domain entity like an order or account), tagged with a sequence number, so a system can fetch “all events for order #4521” and replay them in order to reconstruct that order’s current state.
Rebuilding state: replay and snapshots
Replaying every event from the beginning of time to answer “what is the current balance” works, but it gets slower as the event log grows. The standard fix is snapshotting: periodically persist the computed state (say, every 100 events) so a rebuild only has to replay events since the last snapshot, not the entire history.
This mirrors how database migrations treat schema changes as an ordered sequence rather than in-place edits — the log of changes is the durable record, and the current schema is just where that sequence currently points.
Event sourcing and CQRS
Event sourcing is often paired with CQRS (Command Query Responsibility Segregation), though the two are independent patterns. In this combination, the event log is the write side — every command produces an event — while one or more read-optimized “projections” are built by consuming that log and materializing it into whatever shape queries need: a SQL table for reporting, a search index, a cache. If a new query pattern shows up later, you build a new projection from the existing event history instead of migrating a live table.
This separation is also why event sourcing pairs naturally with the saga pattern for coordinating multi-step transactions across services — each step’s outcome is itself an event other parts of the system can react to.
Common pitfalls
Schema evolution. Events are immutable, but the code that interprets them changes over time. If FundsDeposited gains a new required field, old events don’t have it — consumers need to handle multiple event versions indefinitely, since you can’t rewrite history.
Eventual consistency. Projections built from the event log update asynchronously, so a read model can lag slightly behind the write side. Systems built assuming immediate read-after-write consistency need to account for this explicitly.
Idempotency. Because events can be replayed or delivered more than once (particularly across a distributed message bus), consumers need to handle duplicate delivery without double-applying an effect — see idempotency for the general pattern.
Storage growth. The log only grows; there’s no natural place to delete old events without breaking replay. Snapshotting limits the cost of replay but doesn’t shrink the store itself, so retention policy needs to be a deliberate decision, not an afterthought.
When to use it (and when not to)
Event sourcing earns its complexity when the audit trail itself has business value — financial ledgers, inventory movements, order histories — or when a domain naturally has multiple views over the same underlying activity. It’s a poor fit for simple CRUD domains where “what changed and when” is never actually asked, because it trades a straightforward update statement for an append-only log, a replay mechanism, and a versioning strategy for event schemas. Most systems don’t need it end-to-end; it’s common to apply event sourcing to the one or two aggregates where history matters and use ordinary CRUD everywhere else.
The takeaway
Event sourcing stores state changes as an immutable, ordered log rather than overwriting a current-state record, which makes full history and point-in-time reconstruction a native capability instead of a bolt-on audit table. It pairs well with CQRS for splitting writes (events) from reads (projections), but it introduces real costs — schema evolution, eventual consistency, and unbounded log growth — that only pay off when the history itself is something the business needs to query, not just the current state.
Keep reading
Chisato · · 5 min read What Is CQRS? Command Query Responsibility Segregation
CQRS separates the code paths that change data from the code paths that read it. How it works, why teams adopt it, and when it's overkill.
The Lycoris Team · · 5 min read What Is a Stored Procedure? SQL Logic in the Database
A stored procedure is precompiled SQL saved inside the database and invoked by name, cutting network round trips and centralizing business logic.
The Lycoris Team · · 5 min read What Is Backpressure? Flow Control in Streams and Queues
Backpressure is how a slow consumer signals a fast producer to hold off, preventing memory exhaustion in streams, queues, and network protocols.