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
- 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). - The aggregate.
BankAccountrebuilds 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. - 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.
- Command handlers. The load → fold → decide → append pipeline, with retry on concurrency conflict. This is the whole write side.
- 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.
- 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
- Event-sourced bank account — subject — the self-contained work statement: commands, invariants, examples, acceptance checks.
- CQRS & event sourcing,
DDD, TDD,
BDD — the concept notes whose
# Practicecites this project (event-sourced tests are Given/When/Then natively). - Hexagonal architecture — the store as a driven port.
- Birthday Greetings — hexagonal extraction and Gilded Rose — legacy rescue — sibling exercises.