Event Sourcing¶
Introduction¶
Traditional systems store only the current state — each update overwrites what came before. Event sourcing takes a different approach: every state change is captured as an immutable domain event in an append-only log. The current state is derived by replaying these events:
This gives you a complete audit trail, the ability to reconstruct state at any point in time, and a natural integration point for reactive systems that respond to events as they occur.
Core Concepts¶
- Events are the source of truth. The event log is the primary data store. State (read models, projections) is derived, not stored directly.
- Aggregates guard invariants. An aggregate receives a command, validates business rules against its current state, and produces new events. waku supports both mutable OOP aggregates and immutable functional deciders.
- Optimistic concurrency prevents conflicting writes. Each stream tracks a version number; concurrent updates to the same aggregate are detected and rejected.
- Idempotent appends protect against duplicate events from network retries. Client-provided idempotency keys ensure that retrying the same command is safe.
- Stream length guards prevent unbounded event replay by raising an error when a stream exceeds a configured limit, guiding you toward snapshots.
- Projections transform events into read-optimized views — either inline (same transaction) or via catch-up (eventually consistent background processing).
- Schema evolution is handled through lazy upcasting on read — events are stored in their original form and transformed to the current schema at deserialization time. Snapshot schema versioning with migration chains handles aggregate state structure changes without batch migrations.
The Decider Pattern¶
waku's functional aggregate style is based on the Decider pattern formalized by Jérémie Chassaing:
Decider[State, Command, Event]:
initial_state → State
decide(command, state) → list[Event]
evolve(state, event) → State
Pure functions, no side effects, trivially testable. See Aggregates for both OOP and functional approaches.
Design lineage
waku's event sourcing draws from established frameworks across ecosystems:
- Emmett (TypeScript) — functional-first ES by Oskar Dudycz
- Marten (.NET) — projection lifecycle taxonomy (inline / async / live)
- Eventuous (.NET) —
IEventStore = IEventReader + IEventWriterinterface split - Axon Framework (JVM) — aggregate testing fixtures (Given/When/Then)
- Greg Young — ES + CQRS formalization
Installation¶
Install waku with the event sourcing extra:
For PostgreSQL persistence, also install the SQLAlchemy adapter:
Architecture¶
graph TD
CMD[Command] --> Mediator[Mediator]
Mediator -->|dispatch| Handler[Command Handler]
Handler -->|load| Repo[Repository]
Repo --> Agg[Aggregate]
Agg -->|raise events| Events[Domain Events]
Handler -->|save| Repo
Repo --> Store[Event Store]
Store --> DB[(Storage)]
Store --> Proj[Projections]
Handler -->|publish| Mediator
The extension builds on waku's CQRS module — commands, handlers, and the mediator are all part of the CQRS layer. Event sourcing adds aggregates, an event store, and projections on top:
- Commands enter through the mediator
- Command handlers load aggregates from the repository
- Aggregates validate business rules and raise domain events
- The repository persists events to the event store
- Projections update read models as events are appended
Get started
See Aggregates for a complete walkthrough — from defining events to wiring modules — for both OOP and functional decider styles.
Next steps¶
| Topic | Description |
|---|---|
| Aggregates | OOP aggregates vs functional deciders |
| Event Store | In-memory and PostgreSQL persistence |
| Projections | Build read models from event streams |
| Snapshots | Optimize loading for long-lived aggregates |
| Schema Evolution | Upcasting and event type registries |
| Testing | Given/When/Then DSL for decider testing |