Most applications store data by saving the current state to a database.
When a customer adds an item to a shopping cart, you update a row in the database.
The previous state is lost.
You only see the cart's current state, with all history lost.
This works for many applications. But what happens when you need to know the full history of changes?
What if you need to rebuild past states, debug complex business flows, or generate analytics from historical data?
This is where the Event Sourcing pattern is useful.
Event Sourcing stores every change as an immutable event.
Instead of overwriting data, you append new events to a stream.
The current state is derived by replaying these events.
In this post, I will show you how to build an Event Sourcing application in .NET using the Marten library and PostgreSQL.
We will build a Shopping Cart that demonstrates all the core Event Sourcing concepts: events, streams, aggregates, commands, and projections.
In this post, we will explore:
- What is Event Sourcing
- Overview of the Shopping Cart Application
- Setting Up Marten with PostgreSQL
- Defining Domain Events
- Building the Aggregate
- Writing Commands: Appending Events to Streams
- Building Read Models with Projections
- The Query Side: Reading Projected Data
- When to Use Event Sourcing
- Summary
Let's dive in.
👉 Read original article on my newsletter: https://antondevtips.com/blog/getting-started-with-event-sourcing-in-dotnet-with-marten-and-postgresql
What is Event Sourcing
In traditional CRUD applications, you store the current state of an entity in a database row.
When a customer adds a product to a shopping cart, you insert or update a row in the CartItems table.
When they remove an item, you delete that row.
The database always shows the latest state.
This approach is simple and works well for many use cases. But it has a fundamental limitation: you lose history.
If a customer added 5 items, removed 2, changed quantities 3 times, and then checked out, all you see in the database is the final state.
You cannot answer questions like "What did the cart look like 10 minutes ago?" or "Which items were removed before checkout?"
Event Sourcing takes a different approach. Instead of storing the current state, you store the sequence of events that led to the current state.
For a shopping cart, these events might look like:
- CartCreated - Customer started a new cart
- ItemAdded - Wireless Mouse, quantity 1
- ItemAdded - Mechanical Keyboard, quantity 1
- ItemRemoved - Wireless Mouse
- ItemAdded - Gaming Mouse, quantity 2
- CartCheckedOut - Customer completed the purchase
The current state of the cart is derived by replaying all these events from the beginning. This sequence of events is called an event stream.
Core Concepts
Event Sourcing introduces several core concepts that work together:
Events are immutable facts that describe something that happened in the past.
An event is never deleted or modified.
Once ItemAdded is recorded, it stays in the stream forever.
Events use past tense names: CartCreated, ItemAdded, OrderConfirmed.
Streams are ordered sequences of events that belong to the same entity.
A shopping cart has its own stream. Each stream has a unique identifier, typically a GUID.
All events for cart abc-123 are stored together in correct order as they appear in time.
Aggregates are domain objects that derive their current state by replaying events from a stream.
The ShoppingCart aggregate starts empty and applies each event one by one.
After replaying all events, the aggregate reflects the current state.
Aggregates also enforce business rules before new events are appended.
Projections are read models built from events.
While aggregates give you the state of a single stream, projections can combine data from multiple streams to create optimized views for querying.
For example, a "Product Popularity" projection might track how many times each product was added across all carts.
Event Sourcing is not a replacement for CRUD.
It is a different approach that shines in specific scenarios.
We will explore when to use it at the end of this post.
Now let's build a real application to see these concepts in action.
Overview of the Shopping Cart Application
We will build a Shopping Cart API that demonstrates all the core Event Sourcing concepts.
Here is what our application supports:
Commands (write side):
- Create a new cart
- Add items to the cart
- Remove items from the cart
- Change item quantities
- Checkout the cart
- Confirm the order
Queries (read side):
- Get cart details (from an inline projection)
- Get product popularity rankings (from an async cross-stream projection)
Domain events:
- CartCreated
- ItemAdded
- ItemRemoved
- ItemQuantityChanged
- CartCheckedOut
- OrderConfirmed
Project Structure
The application follows Vertical Slice Architecture with three projects:
ShoppingCart.Domain contains the pure domain model: event records and the aggregate.
ShoppingCart.Features contains the vertical slices: each feature has its own folder with an endpoint, handler, and optional validator. It also contains the projections. This project uses Marten, Carter, ErrorOr, and FluentValidation.
ShoppingCart.WebApi is the host project that wires everything together: Marten configuration, dependency injection, and the ASP.NET Core pipeline.
This structure is similar to what I use in my Modular Monolith projects, but simplified for a single module.
👉 Read original article on my newsletter: https://antondevtips.com/blog/getting-started-with-event-sourcing-in-dotnet-with-marten-and-postgresql
Top comments (0)