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:

spinner

Events as the Source of Truth

Events have three properties that make them the ideal source of truth:

  1. Immutability: Events are facts that have already happened. OrderCreated happened; it cannot un-happen. Events are never updated or deleted.

  2. Completeness: The full history of every state transition is preserved. There is no "lost update" — every change is a record.

  3. Semantic richness: DiscountApplied carries far more meaning than total = 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, not StatusUpdated. DiscountApplied, not DiscountPercentSet.

  • 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