Event sourcing fundamentally changes how you think about your application’s state by treating every change not as a new state, but as an immutable event that happened.

Imagine a simple banking application. Instead of just storing the current balance of an account, event sourcing stores every single transaction: AccountCreated, MoneyDeposited(amount: 100), MoneyWithdrawn(amount: 50), MoneyDeposited(amount: 25). To get the current balance, you simply replay these events from the beginning.

Here’s a look at a simplified event log for an account:

[
  {
    "eventId": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
    "eventType": "AccountCreated",
    "timestamp": "2023-10-27T10:00:00Z",
    "payload": {
      "accountId": "acc-123",
      "initialBalance": 0
    }
  },
  {
    "eventId": "b2c3d4e5-f6a7-8901-2345-67890abcdef1",
    "eventType": "MoneyDeposited",
    "timestamp": "2023-10-27T10:05:00Z",
    "payload": {
      "accountId": "acc-123",
      "amount": 100.50
    }
  },
  {
    "eventId": "c3d4e5f6-a7b8-9012-3456-7890abcdef12",
    "eventType": "MoneyWithdrawn",
    "timestamp": "2023-10-27T10:15:00Z",
    "payload": {
      "accountId": "acc-123",
      "amount": 25.00
    }
  }
]

This approach solves the problem of "what actually happened?" It provides a complete, auditable history of every state change. This isn’t just about what the current state is, but how it got there. This is invaluable for debugging, auditing, and even for building powerful features like time-travel debugging or generating historical reports.

Internally, an event store is essentially an append-only log. New events are always added to the end; they are never modified or deleted. This immutability is the bedrock of event sourcing. When you need to determine the current state of an entity (like an account), you read all the events associated with that entity from the log and apply them in order.

The core levers you control in event sourcing are the event definitions themselves and the logic to project those events into a current state.

  • Event Definitions: These are your domain events. They should be granular, factual statements about something that occurred. For example, OrderPlaced, ItemAddedToOrder, ShippingAddressChanged, PaymentReceived. Each event has a type and a payload containing the relevant data.
  • State Projection/Rehydration: This is the process of taking a stream of events and building the current state object. A common pattern is an apply method on an aggregate root that takes an event and modifies the aggregate’s internal state.

Consider how a simple Account aggregate might be rehydrated:

public class Account
{
    public Guid Id { get; private set; }
    public decimal Balance { get; private set; }
    public int Version { get; private set; } // Often used for optimistic concurrency

    private readonly List<object> _uncommittedEvents = new List<object>();

    // Constructor to load from events
    public Account(IEnumerable<object> history)
    {
        foreach (var ev in history)
        {
            Apply(ev);
            Version++; // Increment version for each applied event from history
        }
    }

    // Apply an event to update state
    private void Apply(object ev)
    {
        switch (ev)
        {
            case AccountCreated created:
                Id = created.AccountId;
                Balance = created.InitialBalance;
                break;
            case MoneyDeposited deposited:
                Balance += deposited.Amount;
                break;
            case MoneyWithdrawn withdrawn:
                Balance -= withdrawn.Amount;
                break;
            // ... other event types
        }
    }

    // Command handlers that generate new events
    public void Deposit(decimal amount)
    {
        if (amount <= 0) throw new ArgumentException("Deposit amount must be positive.");
        var ev = new MoneyDeposited(Id, amount);
        Apply(ev);
        _uncommittedEvents.Add(ev);
        Version++;
    }

    // ... other command handlers
}

When you need the current state of acc-123, you’d fetch all events for acc-123, instantiate Account with those events, and Balance would be correctly calculated.

A surprising consequence of this model is that you can easily build multiple, different "views" of your data from the same event log. For example, you could have one projection that calculates the current account balance, another that generates a monthly statement summary, and a third that tracks fraudulent activity patterns, all derived from the same immutable sequence of events. This is often achieved using read models that subscribe to the event stream and update their own optimized data structures.

The true power of event sourcing lies not just in storing history, but in using that history as the single source of truth to derive all other states and insights.

Want structured learning?

Take the full System Design course →