← Week 3: Event Sourcing & CQRS

Day 15: Event Sourcing

Phase 4 · Aug 5, 2026

← Week 3: Event Sourcing & CQRS

Agenda (2–3 hours)

  • Read (45 min): Martin Fowler "Event Sourcing" pattern; Greg Young "CQRS Documents" (2010) §1–3
  • Study (45 min): Rebuild the state of a shopping cart from an event log by hand; add a snapshot optimization
  • Practice (45 min): Implement a simple event-sourced BankAccount in Rust: events Deposited, Withdrawn, Closed; reconstruct current balance from the event log
  • Challenge (30 min): What is the difference between an event and a command? Why does event sourcing record events (past tense) rather than commands (imperative)?
← Week 3: Event Sourcing & CQRS

Traditional CRUD vs Event Sourcing

CRUD: store current state

accounts: { id: 42, balance: 1000, status: "active" }
UPDATE accounts SET balance = 900 WHERE id = 42; -- information lost!

Event Sourcing: store history of events

events: [
  { account_id: 42, type: "Deposited",  amount: 1000, at: t1 }
  { account_id: 42, type: "Withdrawn",  amount: 100,  at: t2 }
]
current_balance = fold(events) = 900

The current state is derived by replaying the event log. The log is the single source of truth.

← Week 3: Event Sourcing & CQRS

Benefits of Event Sourcing

  1. Temporal queries: "What was the balance at 14:00 yesterday?" — replay up to that point
  2. Audit log built-in: every state change is recorded with who did it and when
  3. Event replay for new projections: add a new read model by replaying historical events
  4. Time-travel debugging: reproduce any state in history
  5. Integration events: emit events to other systems as a natural by-product
← Week 3: Event Sourcing & CQRS

Reconstructing State

#[derive(Debug, Clone)]
enum AccountEvent {
    Deposited { amount: u64, by: String },
    Withdrawn { amount: u64, by: String },
    Closed,
}

#[derive(Default)]
struct BankAccount {
    balance: u64,
    closed: bool,
}

impl BankAccount {
    fn apply(&mut self, event: &AccountEvent) {
        match event {
            AccountEvent::Deposited { amount, .. } => self.balance += amount,
            AccountEvent::Withdrawn { amount, .. } => self.balance -= amount,
            AccountEvent::Closed => self.closed = true,
        }
    }

    fn from_events(events: &[AccountEvent]) -> Self {
        let mut account = Self::default();
        for event in events { account.apply(event); }
        account
    }
}
← Week 3: Event Sourcing & CQRS

Snapshots

Replaying a long event log is expensive. Snapshots short-circuit this:

  1. Periodically serialize the current state: (account_id, sequence_number, state_json)
  2. On load: find latest snapshot → load state → replay only events after snapshot sequence
async fn load_account(id: u64, store: &EventStore) -> BankAccount {
    let (snapshot, since_seq) = store.latest_snapshot(id).await;
    let events = store.events_since(id, since_seq).await;

    let mut account = snapshot.unwrap_or_default();
    for event in events { account.apply(&event); }
    account
}
← Week 3: Event Sourcing & CQRS

Key Takeaways

  • Event sourcing stores events (immutable facts), not current state
  • Current state = fold(events) — reconstructed on demand or cached as a snapshot
  • Benefits: full audit trail, temporal queries, new projections from existing data
  • The event log is append-only — events are never modified or deleted

Tomorrow: CQRS — separating writes (commands) from reads (queries).