Event Sourcing in Distributed Systems: A Practical Guide for Software Developers

Learn how Event Sourcing models business change as an ordered history of events. This tutorial explains event streams, commands, state reconstruction, event stores, domain events, aggregates, .NET implementation, concurrency, snapshots, transactions, projections, read models, failures, eventual consistency, versioning, and schema evolution. It closes by examining real technologies and when Event Sourcing is an appropriate design choice.

1. Lesson 1: Why Event Sourcing Exists Demo 2. Lesson 2: The Core Idea of Event Sourcing Demo 3. Lesson 3: Events, Commands, and State Demo 4. Lesson 4: Rebuilding State from Events Demo 5. Lesson 5: Event Stores Demo 6. Lesson 6: Designing Good Domain Events Locked 7. Lesson 7: Event Sourcing with Aggregates Locked 8. Lesson 8: Implementing Event Sourcing in .NET Locked 9. Lesson 9: Concurrency and Optimistic Concurrency Locked 10. Lesson 10: Snapshots Locked 11. Lesson 11: Event Sourcing and Transactions Locked 12. Lesson 12: Event Sourcing in Distributed Systems Locked 13. Lesson 13: Event Sourcing and Event-Driven Architecture Locked 14. Lesson 14: Projections and Read Models Locked 15. Lesson 15: Handling Failures and Eventual Consistency Locked 16. Lesson 16: Event Versioning and Schema Evolution Locked 17. Lesson 17: Event Sourcing with Real Technologies Locked 18. Lesson 18: When to Use Event Sourcing Locked

Lesson 5: Event Stores

Demo

Once events become the source of truth, we need a reliable place to store and retrieve them. An Event Store is responsible for more than simply saving a collection of records because events must remain ordered, immutable, and associated with the correct stream. This lesson introduces the main responsibilities of an Event Store and explains the concepts that make event persistence reliable.

What Is an Event Store?

In Event Sourcing, events need a reliable place where they can be stored and retrieved in their original order. An Event Store is the persistence mechanism responsible for storing these events and their metadata. Unlike a traditional database table that is usually designed around the current state of an entity, an Event Store is designed around the history of changes.

For example, an order might have this event stream:

Order 5001

1. OrderPlaced
2. OrderPaid
3. OrderShipped

The Event Store keeps these events so that the application can later read the stream and rebuild the order's current state.

Append-Only Storage

A fundamental property of event storage is that events are normally append-only. Once OrderPlaced has been recorded, we do not update it to become OrderPaid. Instead, we append a new OrderPaid event to the stream.

Conceptually, the stream changes like this:

Before:
1. OrderPlaced

After payment:
1. OrderPlaced
2. OrderPaid

After shipping:
1. OrderPlaced
2. OrderPaid
3. OrderShipped

This preserves the historical sequence. If something needs to be corrected, the system normally records another event that represents the correction rather than changing the original event.

Event Streams and Sequence Numbers

Events are usually grouped into event streams, often one stream for a particular aggregate. For example, all events belonging to order 5001 can be stored in its own stream. Each event receives a position or sequence number, which identifies its position within that stream.

Stream: Order-5001

Version 0 -> OrderPlaced
Version 1 -> OrderPaid
Version 2 -> OrderShipped

The sequence number is useful when replaying events because the application knows the correct order in which they should be applied. It is also important for concurrency control because the application can tell whether another operation has changed the stream since it was last read.

Optimistic Concurrency

Consider two application instances working with the same order. Both read the order when its current version is 1. They then independently try to add a new event. Without concurrency protection, both operations could succeed even though one operation was based on an outdated version of the order.

An Event Store can prevent this by allowing the application to say, in effect, "Append this event only if the stream is still at version 1." If another operation has already added an event and moved the stream to version 2, the append fails.

Conceptually, the operation looks like this:

await eventStore.AppendAsync(
    streamId: "Order-5001",
    expectedVersion: 1,
    events: new[] { new OrderPaid(...) });

This is called optimistic concurrency because we do not lock the order while processing it. Instead, we assume conflicts are relatively uncommon and detect a conflict when the application attempts to save its changes.

Event Store Technologies

A dedicated technology such as EventStoreDB provides event streams, ordered events, versions, and concurrency mechanisms as core concepts. However, Event Sourcing does not require a dedicated Event Store product. A relational database such as SQL Server or PostgreSQL can also store events in an append-oriented table.

A simplified relational design might look like this:

CREATE TABLE Events (
    StreamId VARCHAR(100),
    Version INT,
    EventType VARCHAR(100),
    EventData JSON,
    PRIMARY KEY (StreamId, Version)
);

The combination of StreamId and Version can prevent two events from occupying the same position in a stream.

The key idea is that an Event Store is not simply "a database containing JSON." Its important characteristics are the ability to append events, preserve their order, retrieve an event stream, identify event versions, and safely handle concurrent writes. These capabilities provide the foundation that allows an Event-Sourced application to preserve history and reconstruct state reliably.