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 2: The Core Idea of Event Sourcing
Now that we understand some of the limitations of storing only the current state, we can look at the fundamental idea behind Event Sourcing. Instead of treating the latest state as the main piece of information, we can treat each meaningful change as a permanent fact. This lesson introduces that mental model and shows how a sequence of events can become the foundation of an application's data.
State Versus Events
In the previous lesson, we saw that a traditional application usually stores the current state of an object. For example, a bank account might contain Balance = 400. Event Sourcing changes this idea by making the sequence of events the primary source of truth. Instead of asking the database to remember only what the account looks like now, we ask it to remember what happened to the account.
For example, instead of storing only this:
AccountId: 101
Balance: $400
we store something like this:
AccountOpened
MoneyDeposited: $500
MoneyWithdrawn: $100
The $400 balance is now a result of those events rather than the fundamental historical record.
Events Represent Things That Happened
An event represents a fact about something that has already happened in the business. For example, MoneyDeposited means that money was deposited into the account. MoneyWithdrawn means that money was withdrawn, while AccountOpened means that the account was created.
A simple C# representation might look like this:
public record MoneyDeposited(
decimal Amount);
public record MoneyWithdrawn(
decimal Amount);
The important characteristic is that these events are not commands such as DepositMoney. A command asks the system to perform an operation, while an event records that the operation actually happened. We will examine this distinction more carefully in a later lesson.
The Event Stream
Events belonging to a particular entity are normally stored in an ordered sequence called an event stream. For example, the stream for account 101 might look like this:
Account 101
1. AccountOpened
2. MoneyDeposited($500)
3. MoneyWithdrawn($100)
The order matters because events can change the state. If we deposited $500 and then withdrew $100, the result is $400. If we processed the withdrawal before the deposit, the business rules and resulting state could be completely different.
An Event Store such as EventStoreDB is specifically designed around this type of event-stream storage, although Event Sourcing can also be implemented using technologies such as SQL Server or PostgreSQL.
Rebuilding the Current State
If the database stores events instead of the current balance, how does the application know that the balance is $400? It starts with an initial state and applies each event in order. This process is called event replay.
Conceptually, the code might look like this:
var balance = 0m;
foreach (var @event in events)
{
balance = @event switch
{
MoneyDeposited e => balance + e.Amount,
MoneyWithdrawn e => balance - e.Amount,
_ => balance
};
}
After processing MoneyDeposited(500) and MoneyWithdrawn(100), the resulting state is $400. The current state can therefore be reconstructed whenever the application needs it.
The Event Store Becomes the Source of Truth
This leads to the most important principle of Event Sourcing: the event history is the source of truth. If the current balance is $400, we can reconstruct it from the events. If our application loses an in-memory representation of the account, we can rebuild it from the event stream.
This also means that an event should normally not be modified or deleted after it has been stored. MoneyDeposited($500) represents something that actually happened. If the deposit was incorrect, we normally record another event that represents the correction instead of changing the original historical fact.
For example:
MoneyDeposited($500)
MoneyWithdrawn($100)
DepositCorrected(-$50)
The history remains intact, while the business state can evolve.
A Concrete Order Example
Consider an online shopping system. A traditional database might eventually contain:
OrderId: 5001
Status: Shipped
With Event Sourcing, the order might have this event stream:
OrderCreated
ItemAdded(ProductId=10, Quantity=2)
PaymentReceived(Amount=80)
OrderPacked
OrderShipped
The current order state can be reconstructed by applying these events in sequence. More importantly, the system has preserved the business history that explains how the order became Shipped.
The central mental model is therefore simple: traditional persistence stores the result, while Event Sourcing stores the sequence of facts that produced the result. Once this idea is clear, the rest of Event Sourcing becomes much easier to understand because aggregates, event stores, projections, and replay are all built around this fundamental model.