A type-safe event sourcing and CQRS library for Rust with PostgreSQL persistence.
- Strongly-typed aggregates and events - Define your domain model with Rust structs and enums
- Mandatory transactions for ACID guarantees
- Built-in idempotency prevents duplicate event processing
- Optimistic concurrency control detects conflicting modifications
- Transactional outbox pattern for reliable side effects
- Snapshot optimization for fast aggregate loading
- In-memory repository for testing and development
Define your domain aggregate and events:
use eventastic::aggregate::{Aggregate,Context,Root,SideEffect};use eventastic::event::DomainEvent;use eventastic::memory::InMemoryRepository;use eventastic::repository::Repository;#[derive(Clone,Debug)]structBankAccount{id:String,balance:i64,}#[derive(Clone,Debug,PartialEq,Eq)]enumAccountEvent{Opened{event_id:String,account_id:String,initial_balance:i64},Deposited{event_id:String,amount:i64},Withdrawn{event_id:String,amount:i64},}implDomainEventforAccountEvent{typeEventId = String;fnid(&self) -> &Self::EventId{matchself{AccountEvent::Opened{ event_id, .. } => event_id,AccountEvent::Deposited{ event_id, .. } => event_id,AccountEvent::Withdrawn{ event_id, .. } => event_id,}}}// Define a no-op side effect type#[derive(Clone,Debug,PartialEq,Eq)]structNoSideEffect;implSideEffectforNoSideEffect{typeSideEffectId = String;fnid(&self) -> &Self::SideEffectId{unreachable!("No side effects are produced")}}implAggregateforBankAccount{constSNAPSHOT_VERSION:u64 = 1;typeAggregateId = String;typeDomainEvent = AccountEvent;typeApplyError = String;typeSideEffect = NoSideEffect;fnaggregate_id(&self) -> &Self::AggregateId{&self.id}fnapply_new(event:&Self::DomainEvent) -> Result<Self,Self::ApplyError>{match event {AccountEvent::Opened{ account_id, initial_balance, .. } => {Ok(BankAccount{id: account_id.clone(),balance:*initial_balance,})}
_ => Err("Account must be opened first".to_string()),}}fnapply(&mutself,event:&Self::DomainEvent) -> Result<(),Self::ApplyError>{match event {AccountEvent::Opened{ .. } => Err("Account already exists".to_string()),AccountEvent::Deposited{ amount, .. } => {self.balance += amount;Ok(())}AccountEvent::Withdrawn{ amount, .. } => {ifself.balance >= *amount {self.balance -= amount;Ok(())}else{Err("Insufficient funds".to_string())}}}}fnside_effects(&self,_event:&Self::DomainEvent) -> Option<Vec<Self::SideEffect>>{None}}Use the aggregate with transactions:
#[tokio::main]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{let repository = InMemoryRepository::<BankAccount>::new();// Create new account using the Root traitletmut account:Context<BankAccount> = BankAccount::record_new(AccountEvent::Opened{event_id:"evt-1".to_string(),account_id:"acc-123".to_string(),initial_balance:1000,})?;// Deposit money
account.record_that(AccountEvent::Deposited{event_id:"evt-2".to_string(),amount:500,})?;// Save with transactionletmut transaction = repository.begin_transaction().await?;
transaction.store(&mut account).await?;
transaction.commit()?;// Load accountlet loaded_account = repository.load(&"acc-123".to_string()).await?;assert_eq!(loaded_account.state().balance,1500);Ok(())}Eventastic is built around four core concepts:
- Aggregates - Domain entities that apply events to update their state
- Events - Immutable records of what happened in your domain
- Context - Wrapper that tracks aggregate state and uncommitted events
- Repository - Persistence layer with transactional guarantees
Unlike many event sourcing libraries, Eventastic requires transactions for all write operations. This provides:
- ACID compliance - All changes are atomic and consistent
- Idempotency - Duplicate events are detected and handled gracefully
- Concurrency safety - Optimistic locking prevents data races
- Side effect reliability - External operations are processed via outbox pattern
Using Rust provides compile-time guarantees:
- Events must implement required traits (DomainEvent, Clone, etc.)
- Aggregates must handle all event types in match statements
- Error handling is explicit with Result types
- No null pointer exceptions or runtime type errors
Eventastic includes features needed for production systems:
- Automatic snapshot creation and loading
- Comprehensive error types with structured information
- Transaction-based consistency guarantees
The library provides multiple repository implementations:
eventastic::memory::InMemoryRepository- For testing and developmenteventastic_postgres::PostgresRepository- For production PostgreSQL storage with:- Event and snapshot storage with versioning
- Full transaction support with optimistic concurrency control
- Optional encryption for sensitive data
- Database migrations support
eventastic_outbox_postgres::TableOutbox- Transactional outbox pattern for reliable side effect processing
See the examples/ directory for complete implementations:
- Bank - Full banking domain demonstrating:
- Account creation and management
- Transaction processing
- Side effects via outbox pattern
- Idempotency and concurrency handling