In most applications we store the current state: a row in the orders table with status paid and amount 1000. Every change is an UPDATE that overwrites the past: the status was new, it became paid, and the old value is gone forever. Usually that's enough. But sometimes it matters not only what is now, but how we got there. Event Sourcing answers exactly the second question: instead of the current state it stores an immutable stream of events, and computes the state from it.
State versus a stream of events
Picture a bank account. There are two ways to describe it.
The first — name the balance: "the account has 1200". That's state. Compact, but you don't know where the sum came from.
The second — show the statement: "+1000 salary, −300 groceries, +500 refund". That's a stream of events. The balance isn't recorded anywhere separately here — it's obtained by adding up the statement lines. And it's the statement that is primary: the bank keeps the history of operations, and the balance is derived from it.
Event Sourcing brings this idea into code. We don't store "order paid, amount 1000". We store the facts that happened to the order, in the order they happened. And we assemble the current order by "replaying" those facts one after another.
What a stream of events looks like
Events are facts in the past tense: not the command "pay the order", but the accomplished fact "order paid". For a single order the stream might look like this:
OrderCreated { orderId: 42, customer: "Anna" }
ItemAdded { orderId: 42, item: "book", price: 500 }
ItemAdded { orderId: 42, item: "pen", price: 100 }
ItemRemoved { orderId: 42, item: "pen" }
OrderPaid { orderId: 42, amount: 500 }
Each line is immutable: once an event is written, we never touch it again. If the customer changes their mind and removes an item — we don't delete ItemAdded, we append a new event ItemRemoved. History isn't rewritten, it only grows. This is the key property: the past cannot be erased, only a new fact can be added.
The storage for such events is called an event store. In essence it's an append-only table: new events are written to the end, old ones are never changed or deleted.
How state is assembled from events
To learn the current state of an order, we take all its events in order and apply them one by one to an empty template. This process is called replay:
start: empty order
OrderCreated → order 42, customer Anna, no items, total 0
ItemAdded → + book 500 → total 500
ItemAdded → + pen 100 → total 600
ItemRemoved → − pen → total 500
OrderPaid → status: paid, total 500
result: order 42, paid, book, total 500
The logic for applying one event to the state is an ordinary function: "take the current state and an event, return the new state". Running the whole stream through it, we get the up-to-date order. Importantly, the result is deterministic: the same events in the same order always yield the same state.
Projections and the link to CQRS
Replaying the whole stream every time someone opens the order list is expensive and slow. That's why Event Sourcing almost always comes paired with CQRS — splitting writes from reads.
It works like this:
- The command side (write) accepts a command, checks the rules, and appends a new event to the event store. It works only with the stream of facts.
- The query side (read) builds read-friendly tables in advance — projections (also called the read-model). A projection subscribes to the event stream:
OrderPaidarrives — it updates a row in theorders_viewtable, which holds the ready-made state.
The same event log can be replayed into different projections: one for the order list, another for a revenue report, a third for analytics. A new reporting need appears — you write a new projection and replay the whole history into it from scratch, without touching either the command side or the existing projections. This is one of the approach's strengths.
Snapshots: so you don't replay everything from scratch
A long-lived entity can have tens of thousands of events. Replaying them all to restore the state is slow. The solution is a snapshot: periodically save a "cast" of the state at a certain position in the stream.
The analogy is the same bank statement. To learn your balance you don't need to add up operations since the account was opened: you take the balance at the end of last month (a snapshot) and add this month's operations to it.
It's the same here: you saved a snapshot of the order after the 1000th event — and on the next restore you take the snapshot and replay only the events after it. A snapshot is an optimization, not a source of truth: you can always throw it away and rebuild it from the events.
Upsides: full audit and a "time machine"
The main value is that you never lose anything.
- Full audit out of the box. The question "who changed what and when" answers itself — the history of changes is your data model, not a separate log that someone forgot to keep.
- A "time machine". You can restore the state at any past moment: just replay the events up to the point you need. Handy for incident analysis — "what did the order look like yesterday at 15:00?".
- Debugging and reproduction. A bug showed up in production? You can replay the same events locally and see exactly the same state.
- New reports after the fact. You need a metric nobody thought about a year ago — you build a new projection and replay the whole history.
Downsides: why it's expensive and rarely needed
None of this is free, and to be honest: Event Sourcing is needed far more rarely than it seems amid the hype around it.
- Complexity. A developer has to think not "update a field" but "which event happened and how it applies". The entry barrier is higher, debugging is less familiar.
- Event schema evolution. Events are stored forever, and the format changes over time. An
OrderPaidevent from five years ago must still replay today. You have to version events and be able to read old formats — that's ongoing work. - Eventual consistency of projections. Time passes between writing an event and updating the read-model. A user pays for an order, but the list still shows "unpaid" for a couple of seconds. This is eventual consistency, and you have to design the interface around it rather than fight it.
- Infrastructure. The event store, the projection mechanism, snapshots, event delivery — all of it has to be built and maintained. Often Kafka shows up alongside as a transport for events, and with it come its own delivery-reliability concerns.
So Event Sourcing is justified where history is part of the requirements, not a nice bonus: finance and payments, where audit is mandated by law; a complex domain where the cause-and-effect chain of decisions matters; systems that regularly need "how did we get here" analysis. For ordinary CRUD, where the current state is enough, the familiar UPDATE is simpler, cheaper, and more reliable — and that's a perfectly good default.
Where this applies
Event Sourcing is not an "advanced way to store data" but a tool for a specific job: when the history of changes itself has value.
- Payments and accounting. A stream of operations is the natural model, and audit is required by regulation. Here the event model fits perfectly.
- Complex business processes. Orders, insurance cases, applications with a long lifecycle and many transitions — when seeing the whole chain matters, not just the final state.
- Systems that need a "time machine". Where you regularly need to restore the state at a past moment or walk through incidents step by step.
Where beginners stumble:
- They apply Event Sourcing to the whole project "to do it properly", even though 90% of the entities are ordinary CRUD. The event model is applied selectively, to the parts of the domain where history is genuinely needed, not to everything.
- They confuse Event Sourcing with simply sending events to a queue. Publishing
OrderPaidto Kafka is integration between services. Event Sourcing is when the stream of events is your source of truth, from which the state is computed. These are different things, even though the events sound alike. - They forget about event versioning and a year later can't replay the old stream because the event format changed. Compatibility with old events must be planned from the very start.
- They expect instant consistency from the read-model and are surprised that a just-paid order hangs as "unpaid" for a second. The lag of projections is a property of the approach, not a bug.
What to learn next
Event Sourcing rarely lives alone — start with distributed patterns and CQRS, the pair it's usually applied with. The transport for events is often Kafka, so it's worth understanding how message delivery works. The eventual consistency that comes with projections is covered in the system design section, and the reliability of delivery and reprocessing is in the article on timeouts, retries, and idempotency. Finally, the event model is closely tied to domain-driven design: events are the language of the domain, and they are devised together with the business, not after it.