Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Projections

Projections are read models built by replaying events. They’re optimized for queries rather than consistency. A single event stream can feed many projections, each structuring data differently.

The Projection Trait

pub trait Projection: Default + Sized {
    /// Aggregate identifier type this projection is compatible with.
    type Id;
    /// Metadata type expected by this projection
    type Metadata;
}

Projections must be Default because they start empty and build up through event replay.

The ApplyProjection<E> Trait

pub trait ApplyProjection<E>: Projection {
    fn apply_projection(&mut self, aggregate_id: &Self::Id, event: &E, metadata: &Self::Metadata);
}

Unlike aggregate Apply, projections receive:

  • aggregate_id — Which instance produced this event
  • event — The domain event
  • metadata — Store-provided metadata (timestamps, correlation IDs, etc.)

Basic Example

#[derive(Debug, Default)]
pub struct AccountSummary {
    pub accounts: HashMap<String, i64>,
}

impl Projection for AccountSummary {
    type Id = String;
    type Metadata = ();
}

impl ApplyProjection<FundsDeposited> for AccountSummary {
    fn apply_projection(&mut self, id: &Self::Id, event: &FundsDeposited, _: &Self::Metadata) {
        *self.accounts.entry(id.clone()).or_default() += event.amount;
    }
}

impl ApplyProjection<FundsWithdrawn> for AccountSummary {
    fn apply_projection(&mut self, id: &Self::Id, event: &FundsWithdrawn, _: &Self::Metadata) {
        *self.accounts.entry(id.clone()).or_default() -= event.amount;
    }
}

Building Projections

Use ProjectionBuilder to specify which events to load:

let summary = repository
    .build_projection::<AccountSummary>()
    .event::<FundsDeposited>()     // All deposits across all accounts
    .event::<FundsWithdrawn>()      // All withdrawals across all accounts
    .load()
    .await?;

Multi-Aggregate Projections

Projections can consume events from multiple aggregate types. Register each event type with .event::<E>():

let report = repository
    .build_projection::<InventoryReport>()
    .event::<ProductCreated>()   // From Product aggregate
    .event::<SaleRecorded>()     // From Sale aggregate
    .load()
    .await?;

Implement ApplyProjection<E> for each event type the projection handles.

Filtering by Aggregate

Use .events_for() to load all events for a specific aggregate instance:

let account_history = repository
    .build_projection::<TransactionHistory>()
    .events_for::<Account>(&account_id)
    .load()
    .await?;

Using Metadata

Projections can access event metadata for cross-cutting concerns:

#[derive(Debug)]
pub struct EventMetadata {
    pub timestamp: DateTime<Utc>,
    pub user_id: String,
}

#[derive(Debug, Default)]
pub struct AuditLog {
    pub entries: Vec<AuditEntry>,
}

impl Projection for AuditLog {
    type Id = String;
    type Metadata = EventMetadata;
}

impl ApplyProjection<FundsDeposited> for AuditLog {
    fn apply_projection(&mut self, id: &Self::Id, event: &FundsDeposited, meta: &Self::Metadata) {
        self.entries.push(AuditEntry {
            timestamp: meta.timestamp,
            user: meta.user_id.clone(),
            action: format!("Deposited {} to {}", event.amount, id),
        });
    }
}

Projections vs Aggregates

AspectAggregateProjection
PurposeEnforce invariantsServe queries
State sourceOwn events onlyAny events
Receives IDsNo (in envelope)Yes (as parameter)
Receives metadataNoYes
ConsistencyStrongEventual
MutabilityVia commands onlyRebuilt on demand

Next

Stores & Codecs — Event persistence and serialization