Event Sourcing
The Audit Trail Problem That Led Me Here
The POS system needed an audit trail. Every time an order was modified — items added, discount applied, order voided — the client wanted to know who changed what, and when, and what the order looked like before the change.
The obvious approach: a change_log table with old and new values. I built it. It worked for a while. But it was a duplicate of my domain logic — every ORDER update needed a corresponding INSERT into the log. Eventually the log and the actual order fell out of sync during a transaction that failed partway through.
Event sourcing solves this differently: instead of storing the current state and maintaining a separate log, the log is the state. I store every event that happened to an order (OrderCreated, ItemAdded, DiscountApplied, OrderVoided), and the current state is computed by replaying those events. There is no duplication because the audit trail and the state are the same thing.
Table of Contents
What Is Event Sourcing?
In conventional persistence, you store the current state of an entity and overwrite it on every update:
In event sourcing, you store what happened as an append-only log of events:
Current state is derived by reading and replaying all events:
Events as the Source of Truth
Events have three properties that make them the ideal source of truth:
Immutability: Events are facts that have already happened.
OrderCreatedhappened; it cannot un-happen. Events are never updated or deleted.Completeness: The full history of every state transition is preserved. There is no "lost update" — every change is a record.
Semantic richness:
DiscountAppliedcarries far more meaning thantotal = 405. It records intent, not just result.
Aggregates and Event Application
The central concept in event-sourced domain models is the aggregate — an object that records what events happened and applies them to update its internal state.
Practical Example: Order Aggregate
The Event Store
The event store is an append-only table. Nothing is ever updated or deleted.
Rebuilding State From Events
Because the state is derived from events, I can reconstruct any past state of an order by replaying events up to a specific version:
This is how I debug production incidents: "what did order #1234 look like at version 3, before the discount was applied?"
Snapshots for Performance
For aggregates with thousands of events, replaying all events on every load is slow. Snapshots solve this:
Projections and Read Models
Events in the store are the write-side truth. The read side is built by projectors — processes that consume events and maintain denormalised read models.
Projections can be rebuilt at any time by replaying the event stream from the start. This is a powerful capability during schema migrations or bug fixes in the read model.
Event Sourcing and CQRS
Event sourcing and CQRS are independent patterns that work exceptionally well together:
CQRS separates command handlers (writes) from query handlers (reads)
Event sourcing gives the write side an append-only store with full history
Projections connect the two: events from the write side build the read models that queries use
Used together, they provide audit trails, temporal queries, and independently optimised read and write paths.
When to Use Event Sourcing
Event sourcing is a good fit when:
Audit trail is a requirement — who changed what and when, with the ability to reconstruct past state
Temporal queries are needed — "what did the inventory look like at midnight?"
Undo/redo functionality — events can be applied or reversed
Complex domain with many state transitions — the event log makes transitions explicit
Analytics and process mining — event streams are a natural input for analytics pipelines
Compliance in regulated domains — immutable event logs satisfy many audit requirements
Avoid event sourcing when:
Simple CRUD with no audit requirement
The team is not familiar with the pattern and the system is under time pressure
The query patterns are simple and a current-state model serves them well
Storage and replay performance have not been thought through
Lessons Learned
The audit trail problem is the best entry point into event sourcing. If a stakeholder asks "can we see who changed this order?", that is the signal.
Events should be named from the domain's perspective, not the technical perspective.
OrderVoided, notStatusUpdated.DiscountApplied, notDiscountPercentSet.Immutability is non-negotiable. If you start deleting or updating events, you lose the history guarantee that makes event sourcing valuable.
Start without projections, add them when read performance demands it. Replaying 50 events per request is fine. Replaying 5,000 is not.
Upcasting is the migration path. When an event's structure needs to change, write an upcaster that transforms old event payloads to the new format on read — never modify existing events.
Last updated