CQRS and Event Sourcing together don’t just give you an audit trail; they fundamentally change how you reason about your application’s state.

Let’s see this in action. Imagine a simple banking system. Instead of updating a balance field directly, we’ll record every transaction as an event.

Here’s a simplified event store and a command handler:

// Example Event
{
  "eventId": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
  "timestamp": "2023-10-27T10:00:00Z",
  "eventType": "MoneyDeposited",
  "aggregateId": "account-123",
  "version": 1,
  "payload": {
    "amount": 100.50
  }
}

// Command Handler (simplified)
function handleDepositCommand(command) {
  const accountId = command.accountId;
  const amount = command.amount;

  // 1. Load current state by replaying events for accountId
  const events = loadEventsForAccount(accountId);
  let currentBalance = calculateBalanceFromEvents(events);

  // 2. Create a new event
  const depositEvent = {
    eventId: uuid(),
    timestamp: new Date().toISOString(),
    eventType: "MoneyDeposited",
    aggregateId: accountId,
    version: events.length + 1,
    payload: { amount: amount }
  };

  // 3. Save the new event
  saveEvent(depositEvent);

  // 4. Publish the event for read models to consume
  publishEvent(depositEvent);

  return { success: true, accountId: accountId, newBalance: currentBalance + amount };
}

The core idea is that your application’s state is not a snapshot in time, but the sequence of immutable events that led to that state. The "balance" is a derived property, not a primary one.

This solves a few problems simultaneously. First, auditability. Every single change is recorded as an event. Who deposited what, when? It’s right there. Second, reproducibility. If you need to reconstruct the state of an account at any point in history, you simply replay the events up to that point. Third, temporal queries. You can ask "what was the balance of account-123 at 2 PM yesterday?" by replaying events up to that specific timestamp.

The CQRS (Command Query Responsibility Segregation) part comes into play because handling commands (like DepositMoney) is a fundamentally different operation than querying for data (like getting the current balance). Command handlers update the event store. Read models (or "projections") subscribe to these events and build their own optimized data stores for querying. This means your write side is optimized for appending events, and your read side is optimized for fast lookups.

The aggregateId is crucial. It groups related events together. For an Account aggregate, all events related to that specific account will share the same aggregateId. The version ensures that you’re always appending events in the correct order, preventing race conditions on updates. The command handler first loads all historical events for a given aggregateId, determines the current state (e.g., balance) by replaying them, then appends a new event representing the command’s intent. This new event is then persisted, and importantly, published to any interested read models.

When you query for an account’s balance, you’re not reading a balance column. You’re requesting the read model (e.g., a database table optimized for balance queries) to return the current balance. This read model continuously updates itself by listening to events from the event store. If the read model is down for a while, it can catch up by replaying events from where it left off.

The most surprising mechanical aspect is how surprisingly simple the write side can become once you embrace event sourcing. The command handler’s primary job isn’t complex state manipulation; it’s validating a command against the current state (derived from events), creating a new event, and persisting it. The complex "state" logic is distributed across the event replay mechanism and the read models. It’s the read models that handle the "how to display this" part, not the core business logic.

The next hurdle you’ll face is handling eventual consistency between your write and read sides, especially when dealing with complex queries that might span multiple aggregates.

Want structured learning?

Take the full Cqrs course →