Quick Start
Build a simple bank account aggregate in under 50 lines. 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, Projection, Repository,
store::{JsonCodec, 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(Debug, Default, Serialize, Deserialize, sourcery::Aggregate)]
#[aggregate(id = String, error = String, events(FundsDeposited))]
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)]
pub struct TotalDeposits {
pub total: i64,
}
impl Projection for TotalDeposits {
type Id = String;
type Metadata = ();
}
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(JsonCodec);
let mut repository = Repository::new(store);
// Execute a command
repository
.execute_command::<Account, Deposit>(&"ACC-001".to_string(), &Deposit { amount: 100 }, &())
.await?;
// Build a projection
let totals = repository
.build_projection::<TotalDeposits>()
.event::<FundsDeposited>()
.load()
.await?;
println!("Total deposits: {}", totals.total);
assert_eq!(totals.total, 100);
Ok(())
}
What Just Happened?
- Defined an event —
FundsDepositedis a simple struct withDomainEvent - Defined a command —
Depositis a plain struct (no traits required) - Created an aggregate —
Accountuses the derive macro to generate boilerplate - Implemented
Apply— How events mutate state - Implemented
Handle— How commands produce events - Created a projection —
TotalDepositsbuilds a read model - 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, serialization - 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