Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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?

  1. Defined an eventFundsDeposited is a simple struct with DomainEvent
  2. Defined a commandDeposit is a plain struct (no traits required)
  3. Created an aggregateAccount uses the derive macro to generate boilerplate
  4. Implemented Apply — How events mutate state
  5. Implemented Handle — How commands produce events
  6. Created a projectionTotalDeposits builds a read model
  7. Wired the repository — Connected everything with inmemory::Store

Key Points

  • Events are past tense factsFundsDeposited, not DepositFunds
  • Commands are imperativeDeposit, not Deposited
  • The derive macro generates — The event enum, From impls, serialization
  • Projections are decoupled — They receive events, not aggregate types
  • IDs are infrastructure — Passed to the repository, not embedded in events

Next Steps