Event Sourcing in Distributed Systems: A Practical Guide for Software Developers

Learn how Event Sourcing models business change as an ordered history of events. This tutorial explains event streams, commands, state reconstruction, event stores, domain events, aggregates, .NET implementation, concurrency, snapshots, transactions, projections, read models, failures, eventual consistency, versioning, and schema evolution. It closes by examining real technologies and when Event Sourcing is an appropriate design choice.

1. Lesson 1: Why Event Sourcing Exists Demo 2. Lesson 2: The Core Idea of Event Sourcing Demo 3. Lesson 3: Events, Commands, and State Demo 4. Lesson 4: Rebuilding State from Events Demo 5. Lesson 5: Event Stores Demo 6. Lesson 6: Designing Good Domain Events Locked 7. Lesson 7: Event Sourcing with Aggregates Locked 8. Lesson 8: Implementing Event Sourcing in .NET Locked 9. Lesson 9: Concurrency and Optimistic Concurrency Locked 10. Lesson 10: Snapshots Locked 11. Lesson 11: Event Sourcing and Transactions Locked 12. Lesson 12: Event Sourcing in Distributed Systems Locked 13. Lesson 13: Event Sourcing and Event-Driven Architecture Locked 14. Lesson 14: Projections and Read Models Locked 15. Lesson 15: Handling Failures and Eventual Consistency Locked 16. Lesson 16: Event Versioning and Schema Evolution Locked 17. Lesson 17: Event Sourcing with Real Technologies Locked 18. Lesson 18: When to Use Event Sourcing Locked

Lesson 4: Rebuilding State from Events

Demo

Storing events is only part of the Event Sourcing model because the application still needs to know the current state of an aggregate. The interesting part is that this state can be reconstructed from the historical events rather than loaded directly from a state table. In this lesson, we will see how event replay works and how an event stream represents the history of a particular business object.

The Event Stream

In Event Sourcing, the application does not have to store the current state as the primary source of truth. Instead, it stores the sequence of events that happened to an entity. This sequence is called an event stream. For example, an order might have the following stream:

1. OrderPlaced($80)
2. OrderPaid($80)
3. OrderShipped

The order of these events is important because each event can change the state produced by the previous events. The application can therefore start with an empty order and process the events one by one to discover the order's current state.

Starting with an Empty State

Imagine that we have an order with no state initially. We can represent its state in C# like this:

public class OrderState
{
    public Guid Id { get; set; }
    public decimal Amount { get; set; }
    public string Status { get; set; } = "New";
}

When the order is first created, its state might contain an ID, an amount of $80, and a status of Placed. Later events can change that status to Paid and then to Shipped.

The important point is that these values can be calculated from the event history rather than being the original historical data stored in the Event Store.

Replaying the Events

Suppose our event stream contains three events. We can replay them by starting with the initial state and applying each event in order:

var state = new OrderState();

foreach (var @event in events)
{
    switch (@event)
    {
        case OrderPlaced e:
            state.Id = e.OrderId;
            state.Amount = e.Amount;
            state.Status = "Placed";
            break;

        case OrderPaid:
            state.Status = "Paid";
            break;

        case OrderShipped:
            state.Status = "Shipped";
            break;
    }
}

After processing OrderPlaced, the status becomes Placed. After OrderPaid, it becomes Paid, and after OrderShipped, it becomes Shipped. The final state is therefore:

OrderId: 5001
Amount: $80
Status: Shipped

This process of processing historical events to reconstruct state is called event replay.

Why Replay Matters

Replay gives Event Sourcing an important capability. If we lose the in-memory representation of an order, we do not necessarily lose the order's history. We can read the order's event stream from the Event Store and rebuild the state again.

This is also useful when the application needs a new way of looking at historical data. For example, suppose we originally calculated only the order status, but later need to calculate the total number of items purchased. If the necessary information exists in the historical events, we can replay those events using new logic to build a new representation.

Event Replay in Real Systems

Technologies such as EventStoreDB provide concepts specifically designed for event streams, while an application built with .NET can implement the replaying logic itself. In larger systems, replay is also commonly used to rebuild projections or read models after a deployment, bug fix, or change in business requirements.

There is an important trade-off, however. If an order has only a few events, replaying them is extremely cheap. If an aggregate has millions of historical events, replaying the entire stream every time the application needs its state can become expensive. This performance problem leads to the concept of snapshots, which we will discuss later.

The central idea is simple: the event stream is the history, and replaying the event stream produces the current state. Once this relationship is understood, an Event-Sourced system becomes much easier to visualize: events are stored permanently, while the state is reconstructed by applying those events in their original order.