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 gracefully.
The Decider Pattern¶
waku's functional aggregate style is based on the Decider pattern formalized by Jérémie Chassaing:
Decider[Command, State, Event]:
decide(command, state) → list[Event]
evolve(state, event) → State
initial_state → State
Pure functions, no side effects, trivially testable. See Aggregates for both OOP and functional approaches.
Inspiration¶
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
Why waku¶
- Two aggregate styles, one infrastructure. Choose mutable OOP aggregates for simple domains or immutable functional deciders for complex business rules — both share the same event store, projections, and module wiring.
-
Given/When/Then testing DSL. DeciderSpec makes decider tests read like specifications:
-
Lazy schema evolution. Events are stored in their original form — upcasters transform old schemas on read, so you never run batch migrations.
- Full DI integration. Projections, enrichers, stores, and serializers are all resolved through Dishka — swap implementations without touching business logic.
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 |