Event Sourcing
Level: Advanced 60–90 minConcepts: Design PatternsStateBusiness LogicBoundaries
Solutions: C# | TypeScript | Python
Build a simple bank account using event sourcing — where the current state is derived entirely from replaying a sequence of events.
Requirements
Events
The system supports these events:
AccountOpened { accountId, ownerName, timestamp }MoneyDeposited { accountId, amount, timestamp }MoneyWithdrawn { accountId, amount, timestamp }AccountClosed { accountId, timestamp }
Rules
- An account must be opened before any other operation
- Deposits must be positive amounts
- Withdrawals must be positive amounts and cannot exceed the current balance
- A closed account cannot accept deposits or withdrawals
- An account can only be closed if the balance is zero
- The current balance is calculated by replaying all events in order
Projections
Given a list of events, produce:
- Current balance — sum of deposits minus withdrawals
- Transaction history — list of all deposits and withdrawals with running balance
- Account summary — owner name, current balance, number of transactions, account status (open/closed)
Temporal Queries
- Balance at a point in time — given a timestamp, what was the balance at that moment?
- Transactions in a date range — filter the event stream by timestamp
Test Cases
| Events | Expected Balance |
|---|---|
| Open, Deposit $100 | $100 |
| Open, Deposit $100, Deposit $50 | $150 |
| Open, Deposit $100, Withdraw $30 | $70 |
| Open, Deposit $100, Withdraw $100 | $0 |
| Open, Deposit $100, Withdraw $150 | Error: insufficient funds |
| Open, Close | Error: account already closed (on next operation) |
| Deposit without Open | Error: account not found |
| Open, Deposit $100, Close | Error: balance must be zero |
| Open, Deposit $100, Withdraw $100, Close | OK |
Temporal queries:
| Events (with timestamps) | Query | Result |
|---|---|---|
| Open (T1), Deposit $100 (T2), Deposit $50 (T3) | Balance at T2 | $100 |
| Open (T1), Deposit $100 (T2), Withdraw $30 (T3) | Transactions T1–T2 | [Deposit $100] |
Bonus
- Add
MoneyTransferred { fromAccountId, toAccountId, amount, timestamp }— transfers between two accounts - Implement snapshots — periodically save the computed state so replay doesn’t need to start from the beginning
- Add an event store that persists events and supports querying by account ID
- Implement undo — reverse the last event (only if it’s the most recent)
Hint
Start with the simplest projection: replaying events to get a current balance. Get that working with just Open and Deposit before adding withdrawals. The validation rules (insufficient funds, closed account) are separate from the projection — test them independently.
Reference Walkthrough
Full C#, TypeScript, and Python implementations live at tddbuddy-reference-katas/event-sourcing with twenty-four scenarios across all three languages, an EventBuilder for constructing typed events with sensible defaults, and an AccountBuilder that composes event streams and rebuilds aggregates fluently. Domain exceptions (AccountNotOpenException, AccountClosedException, InsufficientFundsException, InvalidAmountException, NonZeroBalanceException) name every rejection. Temporal queries (balance-at-a-point-in-time, transactions-in-range) demonstrate the power of replaying an immutable event stream.
- C# (.NET 8, xUnit, FluentAssertions) — walkthrough
- TypeScript (Node 20, Vitest, strict types) — walkthrough
- Python (3.11, pytest, dataclasses) — walkthrough
This kata ships in middle gear — one commit per language with the full event-sourcing design. See the repo’s Gears section for why that’s a deliberate teaching choice.