Event Sourcing with Protean
Builds on CQRS
Event Sourcing extends the CQRS pathway with event replay, temporal queries, and full audit trails. If you haven't read the CQRS pathway yet, start there — Event Sourcing builds on those concepts.
Hands-On Tutorial
For a hands-on, project-based introduction, see the Event Sourcing Tutorial — a 22-chapter guide that builds a complete banking platform from scratch, covering every Event Sourcing capability in Protean.
Overview
In Event Sourcing, aggregate state is not stored as a snapshot in a database. Instead, every state change is captured as an event, and the aggregate's current state is reconstructed by replaying its event history. This gives you a complete, immutable audit trail and the ability to answer "what was the state at time T?" for any aggregate.
In Protean, you enable Event Sourcing on a per-aggregate basis by marking
it with is_event_sourced=True. The aggregate uses @apply methods to
define how each event type mutates state, and an Event Sourced Repository
handles persistence to the event store.
Request Flow
sequenceDiagram
autonumber
participant API as API Layer
participant D as domain.process()
participant CH as Command Handler
participant ESR as ES Repository
participant ES as Event Store
participant Agg as Aggregate
participant P as Projector
participant Proj as Projection
API->>D: Submit Command
D->>CH: Dispatch to handler
CH->>ESR: Load aggregate
ESR->>ES: Fetch events for stream
ES-->>ESR: Event history
ESR->>Agg: Replay events to rebuild state
CH->>Agg: Invoke domain method
Agg->>Agg: Raise new event(s)
CH->>ESR: Persist aggregate
ESR->>ES: Append new events
ES-->>P: Events dispatched
P->>Proj: Update read model
- The API layer submits a Command via
domain.process() - The Command Handler asks the Event Sourced Repository to load the aggregate
- The repository fetches all events for that aggregate from the Event Store
- It replays them through the aggregate's
@applymethods to rebuild current state - The handler invokes the domain method, which calls
raise_()—raise_()automatically invokes the corresponding@applyhandler, mutating state in-place - The repository appends the new events to the Event Store
- Events flow to Projectors that update read-optimized Projections
What Event Sourcing Adds to CQRS
| CQRS Element | Event Sourcing Change |
|---|---|
@domain.aggregate |
Add is_event_sourced=True option |
| Aggregate methods | Use @apply decorator for event application |
@domain.repository |
Replace with @domain.event_sourced_repository |
| Persistence Store | Replace with Event Store (e.g., Message DB) |
| Everything else | Commands, Command Handlers, Projections, Projectors — unchanged |
Elements You'll Use
Everything from the CQRS pathway, with these changes:
| Element | Purpose |
|---|---|
| Event Sourced Aggregates | Aggregates with is_event_sourced=True that derive state from events |
@apply decorator |
Methods that define how each event type mutates aggregate state — called automatically by raise_() and during replay |
| Event Sourced Repository | Persists events and reconstructs aggregates from event streams |
| Fact Events | Auto-generated snapshot events capturing full aggregate state |
| Event Store adapter | Infrastructure for storing and retrieving events (e.g., Message DB) |
Guided Reading Order
If you've already worked through the CQRS pathway, pick up from step 1 below. These guides cover the Event Sourcing-specific concepts:
Understand the foundations
| Step | Guide | What You'll Learn |
|---|---|---|
| 1 | Event Sourcing Concepts | Theory and workflow of event sourcing |
| 2 | Events | Delta events vs. fact events |
| 3 | Raising Events | How aggregates raise events and the @apply decorator |
Persist and organize events
With the event model understood, learn how events are stored, streamed, and replayed:
| Step | Guide | What You'll Learn |
|---|---|---|
| 4 | Persist Aggregates | Event sourced repositories |
| 5 | Stream Categories | How events are organized into streams |
Build read models and process events
Events are your single source of truth. Use them to power read-optimized views and asynchronous processing:
| Step | Guide | What You'll Learn |
|---|---|---|
| 6 | Projections | Build read models from event streams |
| 7 | Server | Process events asynchronously |
| 8 | Event Store adapters | Configure Message DB or other event stores |
Coordinate, evolve, and test
To manage cross-aggregate workflows, evolve event schemas over time, and verify everything works:
| Step | Guide | What You'll Learn |
|---|---|---|
| 9 | Process Managers | Coordinate multi-step processes across aggregates |
| 10 | Architecture Decision | When to use ES vs. CQRS per aggregate |
| 11 | Event Upcasting | Transform old event schemas during replay |
| 12 | Testing | Test event-sourced aggregates and projections |
Relevant Patterns
These patterns complement the Event Sourcing approach. They span aggregate design, command and event handling, and event schema evolution:
| Pattern | What It Covers |
|---|---|
| Design Small Aggregates | Keep aggregates focused and performant |
| Encapsulate State Changes | Protect aggregate internals with controlled mutation |
| Replace Primitives with Value Objects | Use rich types instead of raw strings and numbers |
| Validation Layering | Apply validation at the right layer |
| Thin Handlers, Rich Domain | Keep handlers lean, push logic into the domain model |
| Testing Domain Logic in Isolation | Test domain rules without infrastructure |
| Organize by Domain Concept | Structure your project around business concepts |
| Creating Identities Early | Generate aggregate IDs before persistence |
| Command Idempotency | Ensure commands can be safely retried |
| Design Events for Consumers | Shape events around downstream needs |
| Idempotent Event Handlers | Handle duplicate events gracefully |
| Coordinating Long-Running Processes | Manage multi-step workflows with process managers |
| Event Versioning and Evolution | Evolve event schemas over time |
Key Concepts
The @apply Decorator
In event-sourced aggregates, state changes are expressed through events.
The @apply decorator marks methods that define how each event type
mutates the aggregate's state. These handlers are invoked automatically
by raise_() during live operations and during event replay —
making them the single source of truth for all state mutations:
@domain.aggregate(is_event_sourced=True)
class Order:
status: String(default="draft")
total: Float(default=0.0)
def place(self):
self.raise_(OrderPlaced(order_id=self.id, total=self.total))
@apply
def placed(self, event: OrderPlaced):
self.status = "placed"
self.total = event.total
@apply
def cancelled(self, event: OrderCancelled):
self.status = "cancelled"
When place() calls raise_(), the framework automatically invokes
placed() to apply the state change. The same placed() method runs
during replay when the aggregate is loaded from the event store. Every
event raised by an ES aggregate must have a corresponding @apply
handler — raising an event without one will throw NotImplementedError.
Fact Events
When fact_events=True is set on an aggregate, Protean auto-generates a
snapshot event after every state change. This captures the complete current
state of the aggregate, making it easy for downstream consumers to build
projections without replaying the full event history.
Mixing Patterns Per Aggregate
Protean allows mixing CQRS and Event Sourcing at the aggregate level within the same domain. Some aggregates can use standard repositories while others use event sourcing — the decision is explicit and per-aggregate:
@domain.aggregate # Standard CQRS — state stored as snapshots
class Product:
...
@domain.aggregate(is_event_sourced=True) # Event Sourced — state from events
class Order:
...
See the Architecture Decision guide for criteria on choosing the right pattern per aggregate.