Skip to content

CQRS / Event Sourcing — Rust

cqrs-rust-lib

Pure CQRS-ESThis implementation
Snapshot = optional optimizationSnapshot = primary read model, written on every command
One command type per operationCreateCommand + UpdateCommand per aggregate
Views updated after event persistenceViewDispatcher fires after (event + snapshot) pair
No prescribed query layeringQuery in domain + QueryBuilder in infrastructure
impl Aggregate for User {
const TYPE: &'static str = "users";
type CreateCommand = CreateUserCommands;
type UpdateCommand = UpdateUserCommands;
type Event = UserEvents;
type Services = Arc<dyn UserServices + Send + Sync>;
type Error = UserError;
async fn handle_create(cmd: Self::CreateCommand, svc: &Self::Services) -> Result<Vec<Self::Event>, Self::Error>;
async fn handle_update(&self, cmd: Self::UpdateCommand, svc: &Self::Services) -> Result<Vec<Self::Event>, Self::Error>;
fn apply(&mut self, event: Self::Event) -> Result<(), Self::Error>;
}

UserQuery — plain data, no DB knowledge — lives in domain. UserQueryBuilder — translates UserQuery to a DB Document — lives in infrastructure.

Carries user identity for audit trails. Extracted in HTTP middleware, threaded through all commands.

ViewDispatcher<A, V, Q> fires after (event + snapshot) pair is persisted.

impl View<Account> for Movement {
const TYPE: &'static str = "movement";
const IS_CHILD_OF_AGGREGATE: bool = true; // true = one record/event; false = one record/aggregate
fn view_id(event: &EventEnvelope<Account>) -> String;
fn update(&self, event: &EventEnvelope<Account>) -> Option<Self>; // None = ignore this event
}

update() must be pure — no I/O, no side-effects.

  • Aggregate + View structs in domain crate — no infra imports
  • QueryBuilder + ViewDispatcher wiring in infrastructure crate
  • Events persisted via engine — no direct snapshot mutation
  • CqrsContext propagated from entry point, never created ad-hoc in domain