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§
Sourcefn name(&self) -> &str
fn name(&self) -> &str
Unique name identifying this projector.
Used for logging, monitoring, position tracking, and rebuild targeting.
Sourcefn handles(&self) -> Vec<String>
fn handles(&self) -> Vec<String>
Event types this projector handles.
Only events whose event_type is in this list will be passed to apply().
Sourcefn 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,
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§
Sourcefn 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,
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".