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

The Aggregate Derive

The #[derive(Aggregate)] macro eliminates boilerplate by generating the event enum and trait implementations.

Basic Usage

use sourcery::Aggregate;

#[derive(Debug, Default, Serialize, Deserialize, Aggregate)]
#[aggregate(id = String, error = String, events(FundsDeposited, FundsWithdrawn))]
pub struct Account {
    balance: i64,
}

Attribute Reference

AttributeRequiredDescription
id = TypeYesAggregate identifier type
error = TypeYesError type for command handling
events(E1, E2, ...)YesEvent types this aggregate produces
kind = "name"NoAggregate type identifier (default: lowercase struct name)
event_enum = "Name"NoGenerated enum name (default: {Struct}Event)

What Gets Generated

For this input:

#[derive(Aggregate)]
#[aggregate(id = String, error = AccountError, events(FundsDeposited, FundsWithdrawn))]
pub struct Account { balance: i64 }

The macro generates:


#[derive(Aggregate)] struct Account

enum AccountEventimpl Aggregate for Accountimpl From<FundsDeposited>impl From<FundsWithdrawn>impl SerializableEventimpl ProjectionEvent

1. Event Enum

pub enum AccountEvent {
    FundsDeposited(FundsDeposited),
    FundsWithdrawn(FundsWithdrawn),
}

2. From Implementations

impl From<FundsDeposited> for AccountEvent {
    fn from(event: FundsDeposited) -> Self {
        AccountEvent::FundsDeposited(event)
    }
}

impl From<FundsWithdrawn> for AccountEvent {
    fn from(event: FundsWithdrawn) -> Self {
        AccountEvent::FundsWithdrawn(event)
    }
}

This enables the .into() call in command handlers:

Ok(vec![FundsDeposited { amount: 100 }.into()])

3. Aggregate Implementation

impl Aggregate for Account {
    const KIND: &'static str = "account";
    type Event = AccountEvent;
    type Error = AccountError;
    type Id = String;

    fn apply(&mut self, event: Self::Event) {
        match event {
            AccountEvent::FundsDeposited(e) => Apply::apply(self, &e),
            AccountEvent::FundsWithdrawn(e) => Apply::apply(self, &e),
        }
    }
}

4. SerializableEvent Implementation

Converts domain events to persistable form:

impl SerializableEvent for AccountEvent {
    fn to_persistable<C: Codec, M>(
        self,
        codec: &C,
        metadata: M,
    ) -> Result<PersistableEvent<M>, C::Error> {
        match self {
            AccountEvent::FundsDeposited(e) => /* serialize with kind */,
            AccountEvent::FundsWithdrawn(e) => /* serialize with kind */,
        }
    }
}

5. ProjectionEvent Implementation

Deserializes stored events:

impl ProjectionEvent for AccountEvent {
    const EVENT_KINDS: &'static [&'static str] = &[
        "account.deposited",
        "account.withdrawn",
    ];

    fn from_stored<C: Codec>(
        kind: &str,
        data: &[u8],
        codec: &C,
    ) -> Result<Self, C::Error> {
        match kind {
            "account.deposited" => Ok(Self::FundsDeposited(codec.deserialize(data)?)),
            "account.withdrawn" => Ok(Self::FundsWithdrawn(codec.deserialize(data)?)),
            _ => /* unknown event error */,
        }
    }
}

Customizing the Kind

By default, KIND is the lowercase struct name. Override it:

#[derive(Aggregate)]
#[aggregate(
    kind = "bank-account",
    id = String,
    error = String,
    events(FundsDeposited)
)]
pub struct Account { /* ... */ }

Now Account::KIND is "bank-account".

Customizing the Event Enum Name

#[derive(Aggregate)]
#[aggregate(
    event_enum = "BankEvent",
    id = String,
    error = String,
    events(FundsDeposited)
)]
pub struct Account { /* ... */ }

Now the generated enum is BankEvent instead of AccountEvent.

Requirements

Your struct must also derive/implement:

  • Default — Fresh aggregate state
  • Serialize + Deserialize — For snapshotting

Each event type must implement DomainEvent.

Next

Manual Implementation — Implementing without the macro