Event sourcing’s power is that every state change is an append-only log, meaning you can rewind and replay history to debug anything.

Let’s watch this happen. Imagine a simple e-commerce system. A customer adds an item to their cart.

{
  "eventId": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
  "eventType": "ItemAddedToCart",
  "timestamp": "2023-10-27T10:00:00Z",
  "aggregateId": "cart-123",
  "version": 1,
  "data": {
    "itemId": "widget-abc",
    "quantity": 1,
    "price": 19.99
  }
}

Later, they check out.

{
  "eventId": "f0e9d8c7-b6a5-4321-0987-654321fedcba",
  "eventType": "OrderPlaced",
  "timestamp": "2023-10-27T10:05:00Z",
  "aggregateId": "cart-123",
  "version": 2,
  "data": {
    "orderId": "order-xyz",
    "items": [
      {
        "itemId": "widget-abc",
        "quantity": 1,
        "price": 19.99
      }
    ],
    "totalAmount": 19.99
  }
}

The aggregateId (cart-123) is the key. It represents the specific entity (the cart) whose state is being modified. The version increments with each change to that aggregate.

This append-only log forms the "source of truth." Instead of updating a database record directly, you emit an event. A separate process (an event handler or projection) then reacts to that event to update its own view of the data (e.g., a read-model for querying).

The problem this solves is that traditional systems often have mutable state. If a bug causes incorrect state, it’s hard to know how it got there. With event sourcing, you have the complete history of how it got there. You can replay events up to a certain point, or even replay events after fixing a bug in your event handling logic, to reconstruct the correct state.

Here’s how you might inspect events in a common event store like EventStoreDB. You’d typically use its client libraries or API. For example, to read events for cart-123:

# Example using a hypothetical CLI tool or API call
eventstore-cli read-stream cart-123 --direction forward --count 10

This would return a stream of events, like the ones shown above, ordered by version. You can see the sequence of changes. If an order was placed but never showed up in the "orders" read-model, you’d look at the OrderPlaced event. Was it even emitted? If so, was the OrderPlaced event handler running correctly?

The core idea is that your application’s state is derived from the sequence of events, not directly stored in a mutable database. You can rebuild any read-model or state at any time by replaying events from the beginning up to the desired point. If a bug is found in the logic that generates a read-model (e.g., it incorrectly calculates tax), you can fix the bug and then re-run the projection over the existing events to correct the read-model’s state without affecting the event log itself.

When debugging, you often need to "replay" events. This isn’t usually a separate action; it’s inherent to how event-sourced systems work. To debug a specific aggregate, like cart-123, you’d retrieve all events for that aggregateId. Then, you’d manually or programmatically apply those events in order to reconstruct the aggregate’s state at any given version. For instance, to see the cart’s state before OrderPlaced, you’d replay only the ItemAddedToCart event.

A common pattern is to have a "dead-letter queue" for events that fail to be processed by handlers. If you see events piling up there, you can inspect them, fix the handler logic, and then resubmit them to the processing pipeline. This is a form of controlled replay.

The most surprising thing is that you can essentially time travel not just through your application’s state, but also through the logic that transforms that state. If you discover a bug in your OrderPlaced event handler that failed to record a specific item, you can fix the handler’s code, and then replay the OrderPlaced event (or a sequence of events leading up to it) against the new code to produce the correct state in your read-model. This is powerful for debugging and for recovering from errors.

The next step is understanding how to build robust read-models (projections) that can be reset and rebuilt from the event stream.

Want structured learning?

Take the full System Design course →