Quick Start
Build a simple bank account aggregate in a compact, end-to-end example. This example demonstrates events, commands, an aggregate, a projection, and the repository.
The Complete Example
use serde::{Deserialize, Serialize};
use sourcery::{Apply, ApplyProjection, DomainEvent, Handle, Repository, store::inmemory};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FundsDeposited {
pub amount: i64,
}
impl DomainEvent for FundsDeposited {
const KIND: &'static str = "account.deposited";
}
#[derive(Debug)]
pub struct Deposit {
pub amount: i64,
}
#[derive(Default, Serialize, Deserialize, sourcery::Aggregate)]
#[aggregate(
id = String,
error = String,
events(FundsDeposited),
derives(Debug, PartialEq, Eq)
)]
pub struct Account {
balance: i64,
}
impl Apply<FundsDeposited> for Account {
fn apply(&mut self, event: &FundsDeposited) {
self.balance += event.amount;
}
}
impl Handle<Deposit> for Account {
fn handle(&self, cmd: &Deposit) -> Result<Vec<Self::Event>, Self::Error> {
if cmd.amount <= 0 {
return Err("amount must be positive".into());
}
Ok(vec![FundsDeposited { amount: cmd.amount }.into()])
}
}
#[derive(Debug, Default, sourcery::Projection)]
#[projection(events(FundsDeposited))]
pub struct TotalDeposits {
pub total: i64,
}
impl ApplyProjection<FundsDeposited> for TotalDeposits {
fn apply_projection(&mut self, _id: &Self::Id, event: &FundsDeposited, _meta: &Self::Metadata) {
self.total += event.amount;
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create an in-memory store
let store = inmemory::Store::new();
let repository = Repository::new(store);
// Execute a command
repository
.execute_command::<Account, Deposit>(&"ACC-001".to_string(), &Deposit { amount: 100 }, &())
.await?;
// Load a projection
let totals = repository.load_projection::<TotalDeposits>(&()).await?;
println!("Total deposits: {}", totals.total);
assert_eq!(totals.total, 100);
Ok(())
}
What Just Happened?
- Defined types —
FundsDepositedevent withDomainEvent,Depositcommand (plain struct) - Created an aggregate —
Accountwith the derive macro,Applyfor state mutations,Handlefor command validation - Created a projection —
TotalDepositsbuilds a read model from events - Wired the repository — Connected everything with
inmemory::Store
Key Points
- Events are past tense facts —
FundsDeposited, notDepositFunds - Commands are imperative —
Deposit, notDeposited - The derive macro generates — The event enum,
Fromimpls, serialisation - Projections are decoupled — They receive events, not aggregate types
- IDs are infrastructure — Passed to the repository, not embedded in events
Next Steps
- Aggregates — Deep dive into the aggregate trait
- Projections — Multi-stream projections
- The Aggregate Derive — Full attribute reference