Event Sourcing for .NET Developers: A Simple Introduction
Learn event sourcing in .NET from scratch. Store every change as an event instead of just the current state, with a real-life bank-passbook analogy, diagrams, code, aggregates, projections, and when to use it.
A bank passbook, not just a balance
Imagine two ways a bank could track your account.
In the first way, the bank stores only one number: your current balance, say ₹5,000. Every time you deposit or withdraw, it simply overwrites that number. Fast and simple — but if there is ever a dispute, the bank cannot tell you how you reached ₹5,000. The history is gone.
In the second way, the bank keeps a passbook: a list of every single transaction. "+₹10,000 salary", "−₹3,000 rent", "−₹2,000 shopping". The current balance is not stored at all — it is calculated by adding up every line. Now the bank can show you exactly how you got to today's balance, replay your history, and even answer questions like "what was my balance last Tuesday?"
That second way is event sourcing. Instead of storing only the current state, you store every change as an event, forever. The current state is whatever you get by replaying all the events. This gives you a perfect history, powerful auditing, and the ability to answer questions you did not even think of when you first built the system.
Let us learn how it works in .NET, step by step.
CRUD vs event sourcing
Normal apps use CRUD storage: Create, Read, Update, Delete. When something changes, you overwrite the old value. The history is lost the moment you update.
The difference is profound. In CRUD, the database row is the truth, and it changes. In event sourcing, the events are the truth, they never change, and the current state is just a calculation on top of them.
The core building blocks
Event sourcing has three main ideas: events, aggregates, and projections. Let us meet each one.
The Three Building Blocks of Event Sourcing
Steps
Events
Immutable facts: 'MoneyDeposited', 'OrderShipped'
Aggregate
Replays its events to know its state and enforce rules
Event Stream
The ordered list of all events for one aggregate
Projections
Read models built from events for fast queries
Events: the facts that happened
An event is a record of something that already happened, written in the past tense. It is immutable — once stored, it never changes.
public record AccountOpened(Guid AccountId, string Owner);
public record MoneyDeposited(Guid AccountId, decimal Amount);
public record MoneyWithdrawn(Guid AccountId, decimal Amount);Notice the names: MoneyDeposited, not DepositMoney. Events describe history, not commands. They are facts, and facts do not change.
Aggregates: replaying events to get state
An aggregate is a unit of related data with its own rules — like a bank account. Its current state is built by replaying its events one by one:
public class BankAccount
{
public Guid Id { get; private set; }
public decimal Balance { get; private set; }
// Rebuild state by applying each event in order
public void Apply(MoneyDeposited e) => Balance += e.Amount;
public void Apply(MoneyWithdrawn e) => Balance -= e.Amount;
public static BankAccount Load(IEnumerable<object> events)
{
var account = new BankAccount();
foreach (var e in events)
account.ApplyEvent(e); // dispatch to the right Apply method
return account;
}
}To make a change, the aggregate checks its rules and then produces a new event rather than mutating a database row:
public MoneyWithdrawn Withdraw(decimal amount)
{
if (amount > Balance)
throw new InvalidOperationException("Insufficient funds");
return new MoneyWithdrawn(Id, amount); // a new fact to append
}Event streams: the history per aggregate
Every aggregate has an event stream — the ordered list of all its events. Loading the aggregate means reading its stream and replaying it.
Projections: making reads fast
Replaying events is great, but what if an aggregate has thousands of events? Replaying them all every time you want to show a balance would be slow. This is where projections come in.
A projection is a read model built from events — a ready-to-read view, shaped for a specific question, kept up to date as new events arrive. Instead of replaying everything on each read, you read the projection directly.
This naturally pairs with CQRS (Command Query Responsibility Segregation): the write side appends events, and the read side queries projections. Each side is optimised for its job.
| Aggregate (write side) | Projection (read side) | |
|---|---|---|
| Purpose | Enforce rules, produce events | Answer queries fast |
| Built from | Its own event stream | One or many event streams |
| Has business logic? | Yes | No — just shaping data |
| Optimised for | Correct writes | Fast reads |
Using a library: Marten
You rarely build an event store by hand. In .NET, Marten (backed by PostgreSQL) is a popular choice. It stores events, replays streams, and can keep projections automatically up to date. A simplified flow looks like this:
// Append events to a stream
session.Events.StartStream<BankAccount>(accountId,
new AccountOpened(accountId, "Asha"),
new MoneyDeposited(accountId, 10000));
await session.SaveChangesAsync();
// Load the aggregate by replaying its stream
var account = await session.Events.AggregateStreamAsync<BankAccount>(accountId);
Console.WriteLine(account!.Balance); // 10000Marten can also maintain a stored projection so you can load the latest state directly like a normal document, instead of replaying every time — giving you both history and speed.
How a command becomes an event
It helps to see the full write flow — what happens when a user asks to do something, like withdraw money:
Handling a Command in Event Sourcing
Steps
Command
A request like 'Withdraw ₹3000' comes in
Load stream
Read all past events for this aggregate
Replay
Rebuild the current state by applying each event
Check rules
Is there enough balance? Enforce business rules
Append
Save a new MoneyWithdrawn event — never overwrite
Read the steps carefully. The aggregate never updates a row. It loads its history, replays it to know where it stands, checks whether the new action is allowed, and then appends a single new fact. The past is never edited — only extended. This append-only nature is what gives event sourcing its perfect history and audit trail.
The benefits of event sourcing
- Complete history. You never lose data. Every change is preserved as an event, forever.
- Perfect audit trail. You can answer "who changed what, and when" precisely — often a legal requirement in finance and healthcare.
- Time travel. You can rebuild the state as it was at any past moment by replaying events up to that time.
- New read models anytime. Need a new report? Build a new projection from the existing events — even for questions you never anticipated.
- Natural fit for events. It pairs beautifully with event-driven systems and the outbox pattern.
The honest trade-offs
Event sourcing is powerful, but it is not free:
- More complexity. It is harder to learn and build than simple CRUD. Replaying, versioning events, and projections all add moving parts.
- Event versioning. Over years, your event shapes change. You must handle old event formats carefully, because you can never delete the history.
- Eventual consistency. Projections often update slightly after the write, so reads can be a touch behind. Your design must accept this.
- Not for everything. Simple data that does not need history is better off as plain CRUD.
| CRUD | Event Sourcing | |
|---|---|---|
| Stores | Current state only | Every change as events |
| History | Lost on update | Kept forever |
| Audit / time travel | Hard | Built in |
| Complexity | Low | Higher |
| Best for | Simple data | Rich, auditable domains |
Snapshots: a speed trick for long streams
What happens when an aggregate has been alive for years and has 100,000 events? Replaying all of them every time you load it would be slow. The common fix is a snapshot.
A snapshot saves the aggregate's state at a certain point — say, after event number 100,000. Next time you load the aggregate, you start from the snapshot and replay only the few events that happened after it, instead of all 100,000.
Without snapshot: replay events 1 ... 100,000 (slow)
With snapshot: load snapshot @100,000
replay events 100,001 ... 100,010 (fast)The events remain the full source of truth — the snapshot is just a performance shortcut you can rebuild or throw away at any time. Libraries like Marten support snapshots and stored projections so you rarely have to manage this by hand, but it is good to know the idea exists for very long-lived aggregates.
You do not need snapshots until streams get long. Start without them, measure your load times, and add snapshots only for the aggregates that genuinely have thousands of events. Premature snapshotting is extra complexity you may never need.
When to use event sourcing
Reach for event sourcing when history and auditability really matter, or when the sequence of changes is itself valuable: banking and payments, order lifecycles, inventory movements, insurance claims, and anything regulated. In these domains, knowing exactly how you reached the current state is as important as the state itself.
Avoid it for simple CRUD apps — a settings screen, a basic catalogue, a contact list. There, the extra complexity buys you nothing, and plain storage is the right tool. A common, wise approach is to use event sourcing only for the few aggregates that truly need it, and CRUD for the rest.
Quick recap
- Event sourcing stores every change as an event, like a bank passbook, instead of overwriting the current state.
- The events are the source of truth; the current state is rebuilt by replaying them.
- An aggregate replays its event stream to know its state and produce new events; projections build fast read models from events.
- It pairs naturally with CQRS and gives you full history, audit trails, and time travel — at the cost of more complexity and eventual consistency.
- Use it where history matters (finance, orders, inventory); stick to CRUD for simple data.
- Libraries like Marten handle the event store and projections for you in .NET.
Keep the full passbook of everything that happened, and you can always replay your way to today's balance — and answer tomorrow's questions about yesterday.
References and further reading
- Event Sourcing pattern — Azure Architecture Center (Microsoft Learn) — Microsoft's official description of the pattern.
- Understanding Event Sourcing with Marten — a friendly introduction using the most popular .NET event store.
- Event Sourcing: Aggregates vs Projections — Domain Centric — a clear deep dive into the two read approaches.
Related Patterns
Getting Started with Event Sourcing in .NET with Marten and PostgreSQL
Learn event sourcing in .NET using Marten and PostgreSQL. Store events, build aggregates and projections, and read state the easy, beginner-friendly way.
CQRS Pattern in .NET: The Way It Should Have Been From the Start
A friendly, hands-on guide to the CQRS pattern in .NET 10. Learn commands, queries, handlers, diagrams, and when to actually use it.
The Outbox Pattern in .NET: Never Lose a Message Again
Learn the Outbox Pattern in .NET with simple, real-life examples. Save your data and your messages in one transaction so a broker outage can never lose an event. Includes EF Core code, diagrams, and best practices.
Stop Conflating CQRS and MediatR: They Are Not the Same Thing
CQRS and MediatR are two different ideas. Learn what each one really does, why people mix them up, and how to use CQRS in .NET with or without MediatR.
Where Vertical Slices Fit Inside the Modular Monolith
A simple guide to how vertical slices live inside the modules of a modular monolith in .NET, with diagrams, code, tables, and everyday examples.
Vertical Slice Architecture in .NET: The Easy Guide
Learn Vertical Slice Architecture in .NET in simple words. Organise code by feature instead of by layer, with diagrams, real examples, a comparison with Clean Architecture, and when to use each.