Skip to content

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
  1. The API layer submits a Command via domain.process()
  2. The Command Handler asks the Event Sourced Repository to load the aggregate
  3. The repository fetches all events for that aggregate from the Event Store
  4. It replays them through the aggregate's @apply methods to rebuild current state
  5. The handler invokes the domain method, which calls raise_()raise_() automatically invokes the corresponding @apply handler, mutating state in-place
  6. The repository appends the new events to the Event Store
  7. 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.