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.

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

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

Benchmarking and Cost Control

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,