rgoussu@goussu: ~/library/applicative-architecture/exercises
~/library/applicative-architecture/exercises cat event-sourced-bank-account.md

Event-sourced bank account

# A bank account built event-sourced from scratch — aggregate, append-only event store, projections — to feel where CQRS and event sourcing pay off and where they cost.

Exercisesaved 2026-08-08 #exercise#event-sourcing#cqrs#ddd#tdd

Goal

Build a small banking service where the event log is the source of truth: commands go through an aggregate that emits domain events, an append-only store persists them, and read models are projections over the stream. Every cost the concept note warns about — concurrency, rebuilds, eventual consistency — shows up concretely, at toy scale, where it can teach instead of hurt.

Subject: full brief & instructions

Practices

  • CQRS & event sourcing — the subject itself: write model, event store, projections, and their real costs.
  • DDD — the aggregate as consistency boundary, events as past-tense facts in the ubiquitous language.
  • TDD — given-events / when-command / then-events as the test template for every behavior.
  • Hexagonal architecture — the event store and projections sit behind driven ports; the domain stays pure.

Milestones

  1. Model the events. AccountOpened, MoneyDeposited, MoneyWithdrawn — immutable, past tense, domain language. Build the given/when/then test helper: given a history of events, when a command, then expect new events (or a rejection).
  2. The aggregate. BankAccount rebuilds state as a left-fold over its events; command methods enforce invariants (no overdraft) and emit events. Pure in-memory, fully TDD'd — no persistence yet.
  3. The event store. Append-only, one stream per account, optimistic concurrency via an expected-version check on append. In-memory first, then file- or SQLite-backed behind the same interface.
  4. Command handlers. The load → fold → decide → append pipeline, with retry on concurrency conflict. This is the whole write side.
  5. Projections. Current-balance and account-statement read models updated from events; handlers idempotent (events will be redelivered); a full rebuild from event zero must reproduce them exactly.
  6. Consistency in the UX. Make read-your-own-writes work after a deposit; add snapshots and measure what they actually buy at this scale.

Stretch goals

  • Evolve an event schema: rename a field on MoneyDeposited, write the upcaster, keep old streams replayable — events are forever.
  • A transfer between two accounts via a process manager: two aggregates, no distributed transaction.
  • Swap the store for a real one (EventStoreDB, Postgres-as-event-store) behind the existing port; compare Greg Young's reference implementation (SimpleCQRS / m-r) with what you built.

Related