Event sourcing is a powerful architectural pattern where state changes are captured as a series of immutable events. Instead of only storing the latest state, we persist the entire history of what happened enabling replay, auditing, and temporal queries.
In this article, I explain how to implement event sourcing in C# using Azure Cosmos DB as the event store backend, including the architecture, repository patterns, data models, and optimizations all written manually without AI assistance.
Why Use Cosmos DB for Event Sourcing?
Cosmos DB is an excellent choice because it offers.
- Global distribution and low-latency reads/writes
- Native JSON document storage, ideal for event payloads
- Automatic partitioning for horizontal scaling
- Tunable consistency and multi-master writes
- A built-in change feed, perfect for driving projections or subscribers
This makes it a great fit for systems that need to store and replay event streams.
Event Store Design
To store event streams, I define the following Cosmos DB document structure.
{
"id": "event-uuid",
"aggregateId": "order-123",
"aggregateType": "Order",
"eventType": "OrderPlaced",
"eventNumber": 1,
"timestamp": "2024-05-01T12:00:00Z",
"payload": { /* event-specific data */ }
}
Partitioning Strategy
- Partition key: /aggregateId → ensures all events for an aggregate are colocated for efficient querying.
- Event ordering: maintained using eventNumber.
This schema allows me to fetch all events for a given aggregate with a simple range query.
C# Event Store Repository Implementation
I implement the repository layer using the Azure Cosmos DB SDK (Azure.Cosmos).
Event Envelope Class
public class EventEnvelope
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string AggregateId { get; set; } = string.Empty;
public string AggregateType { get; set; } = string.Empty;
public string EventType { get; set; } = string.Empty;
public int EventNumber { get; set; }
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
public object Payload { get; set; } = new();
}
Event Store Repository Class
using Azure.Cosmos;
public class EventStoreRepository
{
private readonly CosmosContainer _container;
public EventStoreRepository(CosmosClient client, string databaseId, string containerId)
{
_container = client.GetDatabase(databaseId).GetContainer(containerId);
}
public async Task AppendEventAsync(EventEnvelope evt)
{
await _container.CreateItemAsync(evt, new PartitionKey(evt.AggregateId));
}
public async Task<List<EventEnvelope>> GetEventsAsync(string aggregateId)
{
var query = _container.GetItemLinqQueryable<EventEnvelope>(allowSynchronousQueryExecution: false)
.Where(e => e.AggregateId == aggregateId)
.OrderBy(e => e.EventNumber)
.ToFeedIterator();
var results = new List<EventEnvelope>();
while (query.HasMoreResults)
{
var response = await query.ReadNextAsync();
results.AddRange(response);
}
return results;
}
}
Optimizations and Best Practices
- Optimistic concurrency: include the last known event number in write operations to detect and prevent conflicting updates.
- Event versioning: evolve event schemas carefully; store a schemaVersion field if needed.
- Minimize payload size: keep only necessary data in events; archive heavy snapshots separately.
- Leverage change feed: use Cosmos DB’s change feed to drive real-time projections or subscribers.
Benchmarking and Cost Control
- Tune RU/s (request units) carefully based on write/read volume.
- Optimize indexing policies to focus on fields like aggregateId and eventNumber.
- Use Batch operations if appending multiple events simultaneously.
Final Takeaway
By combining solid event sourcing patterns with Cosmos DB’s scalable document database, you can build highly resilient, auditable, and replayable systems. Using C# and the Cosmos DB SDK, I can construct a fully functioning event store that integrates cleanly into microservice or domain-driven architectures.
If you want, I can also provide detailed examples on,
- Building projections
- Handling event replay and rehydration
- Integrating Cosmos DB change feed listeners
- Writing integration tests for the event store