Module 17 · Lesson 1 of 4
Event sourcing
Store what happened, derive what is.
A fan writes to support: "I booked A12 and A13, but my ticket says A12 and A14." The bookings row says A12 and A14, and that is all it can say, because every update overwrote the one before. The business wants a full history of every booking change. Event sourcing gives you that history by making it the only thing you store.
Store what happened, not just what is
A normal table stores current state. When the fan swaps a seat, you run UPDATE bookings SET seats = '{A12,A14}', and the fact that A13 was ever involved is gone. You can bolt on an audit table, but then you have two sources of truth that can drift apart.
Event sourcing turns this around. The source of truth is an append-only list of events, the facts about what happened, in order:
SeatHeld { seat: A12 }SeatHeld { seat: A13 }SeatReleased { seat: A13 }SeatHeld { seat: A14 }PaymentCaptured { amount: 1800 }TicketIssued
Current state isn't stored at all. It is derived by starting from an empty booking and applying each event in order. That operation, folding a list of events into a value, is the heart of the pattern. In code it is a function apply(state, event) → state, and current state is events.reduce(apply, empty).
If this feels familiar, it should. A bank account is event-sourced: the ledger of deposits and withdrawals is the truth, and the balance is computed from it. The Kafka log is the same shape, and so is a database's own write-ahead log. Event sourcing applies that idea to your business data.
Rebuilding state, and time travel
To load booking b_8812, the service reads its events and folds them. To see what the booking looked like at 10:41:30, it folds only the events up to that time. You get "what did we know then?" for free, which is exactly what support needs to answer the fan's question: they held A13 for 35 seconds, released it and picked A14.
Play with it below. Append events with the commands, then drag the slider back.
Try it
One booking, stored as events
Append events with the commands, then drag the slider back in time. The state on the right is rebuilt from the log every time.
Event store · booking b_8812 · append-only
State now
- Status
- ticketed
- Seats
- A12, A14
- Paid
- ₹1,800
- Refunded
- ₹0
rebuilt by applying 6 events from empty
A booking has a handful of events, so folding them is instant. A long-lived entity, such as a show's inventory with 40,000 seat events, would take too long to replay on every request. So you take snapshots: every N events, store the folded state with the event number it covers. Loading becomes "read the latest snapshot, then apply the few events after it". Snapshots are a cache, not a source of truth. You can delete them all and rebuild from events. Tick the snapshot box in the visual to see how many events each rebuild applies.
Commands become events
Users don't send events; they send commands: "hold seat A15", "cancel my booking". A command is a request, and it can be refused. The service loads the current state, checks the rules (is A15 free? is this booking already cancelled?), and if the command is valid, appends one or more events. The visual does the same: buttons that would break a rule are disabled.
Once appended, an event is a fact and is never refused again. When you replay events, you don't re-check business rules, because the rules might have changed since. A refund issued last year under last year's policy still happened.
Two requests for the same booking can arrive at once. To stop both from appending conflicting events, the append carries the version the command was checked against: "append SeatHeld A15, expecting the stream to be at version 6". If someone else appended event 7 first, the append fails, and the service reloads and tries again. This is optimistic concurrency, and event stores support it directly.
Commands are requests you may reject. Events are facts you never change. State is whatever you get by replaying the facts.
What you get, and what it costs
The benefits are real:
- A complete audit trail that can't drift from the data, because it is the data.
- Time travel for support, debugging and disputes.
- New views from old data. Product wants "how often do fans swap seats before paying?" A normal table can't answer that. With events, you write a new projection and run it over all of history.
- Natural integration with messaging. The events you store are the events you publish.
And so are the costs, which is why most Gatepass services don't use it:
- Events are forever. You can't change an event's shape after the fact, because old events are still on disk. New code must read every version that ever existed, often by upcasting old events to the new shape as they load.
- Querying is hard. "All bookings for show 17 in row A" can't be answered by scanning event streams. You need separate read models built from the events, which is the next lesson, CQRS.
- Deleting is hard. Privacy laws may require erasing a fan's personal data from an append-only log. A common trick is to encrypt personal fields with a per-fan key and throw the key away.
- It's unfamiliar. Most engineers think in tables. Debugging a fold is a new skill.
Use it where history is the product: bookings, payments, ledgers, inventory. Don't use it for an organiser's profile page.
Check yourself
Gatepass changes its rules: from today, bookings are limited to 4 seats. Last year's events include a booking with 6 SeatHeld events. What happens when that booking is replayed?
Your turn
List the events you would store for a show (not a booking): from the organiser creating it to the night it happens. Name each as a past-tense fact with its key fields.
Key takeaways
- Event sourcing stores an append-only list of events as the source of truth. Current state is a fold over them.
- You get a complete audit trail, time travel, and the ability to build new views from all of history.
- Commands are validated and may be rejected; events are facts and are replayed without re-checking rules.
- Snapshots speed up rebuilding long streams; optimistic concurrency stops conflicting appends.
- Costs: events are forever, queries need separate read models, and deletion needs care. Use it where history matters.
Finished reading?
Mark it complete to tick it off in the course.