Skip to main content

Projector

Trait Projector 

Source
pub trait Projector: Send + Sync {
    // Required methods
    fn name(&self) -> &str;
    fn handles(&self) -> Vec<String>;
    fn apply<'life0, 'life1, 'life2, 'async_trait>(
        &'life0 self,
        event: &'life1 Event,
        store: &'life2 dyn ReadModelStore,
    ) -> Pin<Box<dyn Future<Output = ProjectionResult<()>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait,
             'life2: 'async_trait;

    // Provided method
    fn init<'life0, 'life1, 'async_trait>(
        &'life0 self,
        _store: &'life1 dyn ReadModelStore,
    ) -> Pin<Box<dyn Future<Output = ProjectionResult<()>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait { ... }
}
Expand description

A projector contains the pure event-handling logic for building a read model.

Projectors are stateless — they receive events and translate them into write operations against a ReadModelStore. They do not own the store or the read model state.

§Design

  • Stateless: all state lives in the ReadModelStore
  • Deterministic: same events + empty store = same read model
  • Composable: one projector per read model concern
  • &self: safe to share across threads

§Idempotency

apply() should be idempotent — handling the same event twice must produce the same result. Use UPSERT, check event_id, or make operations naturally idempotent (SET vs INCREMENT).

§Example

struct UserListProjector;

#[async_trait]
impl Projector for UserListProjector {
    fn name(&self) -> &str { "UserList" }

    fn handles(&self) -> Vec<String> {
        vec!["UserCreated".to_string()]
    }

    async fn apply(&self, event: &Event, store: &dyn ReadModelStore) -> ProjectionResult<()> {
        store.upsert(Upsert::new("users_view", &event.aggregate_id, event.payload.clone())).await
            .map_err(|e| ProjectionError::handle_failed(
                "UserList", &event.event_type, &event.event_id.to_string(), e.to_string()
            ))?;
        Ok(())
    }
}

Required Methods§

Source

fn name(&self) -> &str

Unique name identifying this projector.

Used for logging, monitoring, position tracking, and rebuild targeting.

Source

fn handles(&self) -> Vec<String>

Event types this projector handles.

Only events whose event_type is in this list will be passed to apply().

Source

fn apply<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, event: &'life1 Event, store: &'life2 dyn ReadModelStore, ) -> Pin<Box<dyn Future<Output = ProjectionResult<()>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Apply a single event to the read model via the store.

This method should be idempotent: applying the same event twice must produce the same result.

Provided Methods§

Source

fn init<'life0, 'life1, 'async_trait>( &'life0 self, _store: &'life1 dyn ReadModelStore, ) -> Pin<Box<dyn Future<Output = ProjectionResult<()>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Initialize the read model schema (CREATE TABLE IF NOT EXISTS, etc.).

Called once when the projector is first registered and before rebuilds. Default implementation does nothing (for stores that don’t need schema setup).

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§