Event sourcing flips how you think about state. Instead of storing the current state of something, you store every single change that ever happened to it, as an immutable sequence of events.

Let’s see this in action. Imagine a simple bank account.

{
  "accountId": "acc-123",
  "events": [
    {
      "eventType": "AccountCreated",
      "timestamp": "2023-10-27T10:00:00Z",
      "payload": {
        "initialBalance": 100.00,
        "currency": "USD"
      }
    },
    {
      "eventType": "MoneyDeposited",
      "timestamp": "2023-10-27T10:15:00Z",
      "payload": {
        "amount": 50.00
      }
    },
    {
      "eventType": "MoneyWithdrawn",
      "timestamp": "2023-10-27T11:00:00Z",
      "payload": {
        "amount": 25.00
      }
    }
  ]
}

To get the current balance, you simply replay these events in order. AccountCreated sets the balance to 100. MoneyDeposited adds 50, making it 150. MoneyWithdrawn subtracts 25, leaving 125. The event log is the source of truth.

This approach solves a fundamental problem: auditability and understanding the how and why behind state changes, not just the what. Traditional databases tell you "the balance is 125," but not how it got there. An event log tells you: "It started at 100, someone added 50, then someone took out 25." This is invaluable for debugging, business intelligence, and even for reconstructing past states.

Internally, an event store is typically a database optimized for appending events. Think of it like a journal. Each event has a unique ID, a timestamp, and the type of change it represents, along with any data needed to apply that change. When you need the current state of an aggregate (like our bank account), you query the event store for all events associated with that aggregate’s ID and apply them sequentially. This process is called "rehydration."

The levers you control are primarily the design of your events and how you handle projections. Event design is crucial: events should be facts about something that has happened. "MoneyDeposited" is good; "DepositMoney" is not. Projections are read-optimized views derived from the event stream. For our bank account, one projection might be the current balance, another might be a list of all transactions for reporting. You build and maintain these projections by subscribing to the event stream and updating your read models as new events arrive.

One fundamental challenge is managing the schema of your events over time. As your application evolves, you’ll want to change event structures. The key is that old events remain immutable. You handle schema evolution by versioning your events and writing "upcasters" or "downcasters" that transform older event versions into the current schema when replaying them. This ensures that even if an event was recorded with an older format, your application can still understand and process it correctly when rehydrating an aggregate or rebuilding a projection.

The next hurdle you’ll face is handling eventual consistency when using projections for querying.

Want structured learning?

Take the full Event-driven course →