In traditional CRUD applications, the database stores the current state of an entity. If John changes his shipping address, the old address is overwritten and lost forever. This is catastrophic for banking, e-commerce, or legal systems where an audit trail is mandatory. Event Sourcing solves this by storing the history of events rather than the current state. CQRS (Command Query Responsibility Segregation) pairs with this to ensure your system can read this data at lightning speed.


Module 1: The Philosophy of Event Sourcing

Instead of storing { id: 1, balance: $100 } in a Accounts table, you store a sequence of immutable events in an EventStore table.

The Event Log

  • Event 1: AccountCreated (id: 1, initialBalance: $0)
  • Event 2: FundsDeposited (id: 1, amount: $150)
  • Event 3: FundsWithdrawn (id: 1, amount: $50)

To get the current balance, you load all events for ID 1 and 'reduce' or 'replay' them in memory (150 - 100). Because events are immutable, you can travel back in time to see exactly what the balance was on Tuesday at 4:00 PM.


Module 2: Implementing CQRS

Replaying 10,000 events every time a user wants to check their balance is too slow. This is where CQRS comes in. CQRS splits your application into two strict halves: The Command side (Writes) and the Query side (Reads).

The Write Model (Command)javascript
// 1. The user issues a COMMAND
async function handleWithdrawalCommand(accountId, amount) {
  // Load all past events to ensure they have enough money
  const events = await EventStore.getEvents(accountId);
  const currentBalance = events.reduce((balance, event) => {
    if (event.type === 'Deposited') return balance + event.amount;
    if (event.type === 'Withdrawn') return balance - event.amount;
    return balance;
  }, 0);

  if (currentBalance < amount) throw new Error("Insufficient funds");

  // 2. Append the new EVENT to the Event Store
  const newEvent = { type: 'Withdrawn', accountId, amount, timestamp: Date.now() };
  await EventStore.append(newEvent);

  // 3. Publish the event to a Message Broker (Kafka/RabbitMQ)
  await MessageBroker.publish('AccountEvents', newEvent);
}

Module 3: The Read Model (Projections)

The Query side listens to the Message Broker and builds a 'Projection'. A Projection is a heavily optimized, read-only database (like Redis or MongoDB) that simply stores the current state.

The Read Model (Projection)javascript
// This microservice runs separately and listens to the Kafka topic
MessageBroker.subscribe('AccountEvents', async (event) => {
  if (event.type === 'Withdrawn') {
    // Instantly update the read-only database
    await ReadDatabase.accounts.updateOne(
      { id: event.accountId },
      { $inc: { balance: -event.amount } } 
    );
  }
});

// 4. The user issues a QUERY
app.get('/api/balance/:id', async (req, res) => {
  // Lightning fast read. No event replaying required.
  const account = await ReadDatabase.accounts.findById(req.params.id);
  res.json({ balance: account.balance });
});

Module 4: Eventual Consistency

Because the Command side writes to the Event Store, and the Query side updates the Read Database via a message broker, there is a delay (usually <50ms). This means CQRS systems are Eventually Consistent. If a user withdraws money and immediately refreshes their browser, they might briefly see their old balance.