Event Sourcing & CQRS · Event Sourcing Concepts
What Event Sourcing Is
Store every change as an immutable event and derive state from them.
In event sourcing, the source of truth is an append-only sequence of domain events (AccountOpened, MoneyDeposited, MoneyWithdrawn) per entity (a stream). Current state is derived by replaying (folding) events. Benefits: complete audit history, temporal queries ("what was the balance on March 1?"), the ability to build new read models from history, and natural event publishing. Costs: more complex design, eventual consistency for read models, event schema evolution and harder ad-hoc queries.
CRUD vs event sourcing
| Criterion | CRUD (current state) | Event sourcing |
|---|---|---|
| Source of truth | Latest row values | Append-only events |
| History / audit | Separate audit tables (often incomplete) | Built in, complete |
| Temporal queries | Hard | Replay to any point |
| New read models from the past | Impossible for lost data | Replay history |
| Ad-hoc queries | Easy (SQL on tables) | Needs projections |
| Complexity | Low | High |
from dataclasses import dataclass
@dataclass
class Account:
id: str
balance: int = 0
closed: bool = False
def apply(state: Account, event: dict) -> Account:
match event["type"]:
case "AccountOpened": return Account(id=event["account_id"])
case "MoneyDeposited": return Account(state.id, state.balance + event["amount"], state.closed)
case "MoneyWithdrawn": return Account(state.id, state.balance - event["amount"], state.closed)
case "AccountClosed": return Account(state.id, state.balance, True)
return state
def load(events: list[dict]) -> Account:
state = Account(id="")
for e in events:
state = apply(state, e)
return state
type Event =
| { type: "AccountOpened"; accountId: string }
| { type: "MoneyDeposited"; amount: number }
| { type: "MoneyWithdrawn"; amount: number };
interface Account { id: string; balance: number }
const evolve = (s: Account, e: Event): Account => {
switch (e.type) {
case "AccountOpened": return { id: e.accountId, balance: 0 };
case "MoneyDeposited": return { ...s, balance: s.balance + e.amount };
case "MoneyWithdrawn": return { ...s, balance: s.balance - e.amount };
}
};
export const rehydrate = (events: Event[]) => events.reduce(evolve, { id: "", balance: 0 });
Common pitfalls
- Using event sourcing everywhere instead of in complex/audit-heavy domains.
- Events that are just CRUD diffs ('FieldChanged').
- Treating a message broker as the event store.
Key points
- Events are the source of truth
- State = fold over events
- Powerful but costly — use selectively
Further reading: Martin Fowler: Event Sourcing
Practice questions
How do you get an entity's current state in an event-sourced system?
Answer
Load its event stream and apply each event in order to an initial state (optionally starting from a snapshot).
Name two capabilities event sourcing gives that CRUD lacks.
Answer
A complete, reliable audit trail and the ability to reconstruct past states or build new read models by replaying history.
Open in the app to mark lessons complete, take notes and track your progress.