Event Sourcing is an architectural pattern where the state of an application is determined by a sequence of events rather than just storing the current state. Instead of updating a record in place, every change is captured as an immutable event and appended to an event store.
The traditional approach to persistence:
- Maps classes to database tables
- Fields of those classes to table columns
- Instances of those classes to rows in those tables
The Trouble with Traditional Persistence
- Object-Relational impedance mismatch
- Lack of aggregate history
- Implementing audit logging
- Lack of event publishing
- When an application creates or updates an aggregate, it inserts the events emitted by the aggregate into the EVENTS table
- An application loads an aggregate from the event store by retrieving its events and replaying them to build its latest state
- Loading an aggregate consists of the following three steps:
- Load the events for the aggregate
- Create an aggregate instance by using its default constructor
- Iterate through the events, calling
apply()
- Reliably publishes domain events
- Preserves the history of aggregates
- Mostly avoids the O/R impedance mismatch problem
- Provides developers with a time machine
- Handling concurrent updates
- Performance
- Idempotent message processing
- Evolving domain events
EventFlow is a popular .NET library for building event-sourced applications. It provides:
- Aggregates: Domain objects that encapsulate business logic and emit events
- Commands & Command Handlers: Encapsulate user intentions and execute business logic
- Events: Immutable records of state changes
- Read Models: Optimized projections for querying
- Event Store: Pluggable storage (PostgreSQL, SQL Server, EventStore, etc.)
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Command │────▶│ Aggregate │────▶│ Domain Event │
│ (Intent) │ │ (Business │ │ (State Change) │
└─────────────┘ │ Logic) │ └────────┬────────┘
└──────────────┘ │
▼
┌──────────────┐ ┌─────────────────┐
│ Read Model │◀────│ Event Store │
│ (Query View) │ │ (PostgreSQL) │
└──────────────┘ └────────┬────────┘
│
▼
┌─────────────────┐
│ Kafka │
│ (Integration) │
└─────────────────┘
- Command:
CreateCustomerCommandis sent with customer details - Aggregate:
CustomerAggregatevalidates and emitsCustomerCreatedEvent - Event Store: Event is persisted to PostgreSQL
- Read Model:
CustomerReadModelis updated for fast queries - Integration: Event is published to Kafka for other services
- .NET 9 SDK
- Docker & Docker Compose
docker compose --profile infra up -dThis starts:
- Redpanda (Kafka) on port 9092
- PostgreSQL on port 5433
- Redpanda Console on port 8080
cd src/Customer.API
dotnet ef database update --project ../Customer.Infrastructurecd src/Customer.API
dotnet run| Method | Endpoint | Description |
|---|---|---|
| POST | /customer |
Create a new customer |
| GET | /customer/{id} |
Get customer by ID |
| POST | /customer/{id}/email |
Update customer email |
| DELETE | /customer/{id} |
Delete a customer |