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.
Lesson 4: Rebuilding State from Events
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.