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

Commands

Commands represent requests to change the system. Unlike events (which are facts), commands can be rejected. An aggregate validates a command and either produces events or returns an error.

The Handle<C> Trait

pub trait Handle<C>: Aggregate {
    /// Handle a command and produce events.
    ///
    /// # Errors
    ///
    /// Returns `Self::Error` if the command is invalid for the current
    /// aggregate state.
    fn handle(&self, command: &C) -> Result<Vec<Self::Event>, Self::Error>;
}

Key points:

  • Takes &self — the handler reads state but doesn’t mutate it
  • Returns Vec<Event> — a command may produce zero, one, or many events
  • Returns Result — commands can fail validation

Command Structs

Commands are plain structs. No traits required:

#[derive(Debug)]
pub struct Deposit {
    pub amount: i64,
}

#[derive(Debug)]
pub struct Withdraw {
    pub amount: i64,
}

#[derive(Debug)]
pub struct Transfer {
    pub to_account: String,
    pub amount: i64,
}

Implementing Handlers

Each command gets its own Handle implementation:

impl Handle<Deposit> for Account {
    fn handle(&self, cmd: &Deposit) -> Result<Vec<Self::Event>, Self::Error> {
        if cmd.amount <= 0 {
            return Err(AccountError::InvalidAmount);
        }
        Ok(vec![FundsDeposited { amount: cmd.amount }.into()])
    }
}

impl Handle<Withdraw> for Account {
    fn handle(&self, cmd: &Withdraw) -> Result<Vec<Self::Event>, Self::Error> {
        if cmd.amount <= 0 {
            return Err(AccountError::InvalidAmount);
        }
        if cmd.amount > self.balance {
            return Err(AccountError::InsufficientFunds);
        }
        Ok(vec![FundsWithdrawn { amount: cmd.amount }.into()])
    }
}

The .into() call converts your concrete event type into the aggregate’s event enum (generated by the derive macro).

Multiple Events

A single command can produce multiple events:

impl Handle<CloseAccount> for Account {
    fn handle(&self, _cmd: &CloseAccount) -> Result<Vec<Self::Event>, Self::Error> {
        if self.balance < 0 {
            return Err(AccountError::NegativeBalance);
        }

        let mut events = Vec::new();

        // Withdraw remaining balance
        if self.balance > 0 {
            events.push(FundsWithdrawn { amount: self.balance }.into());
        }

        // Mark as closed
        events.push(AccountClosed {}.into());

        Ok(events)
    }
}

No Events

Return an empty vector when the command is valid but produces no change:

impl Handle<Deposit> for Account {
    fn handle(&self, cmd: &Deposit) -> Result<Vec<Self::Event>, Self::Error> {
        if cmd.amount == 0 {
            return Ok(vec![]); // Valid but no-op
        }
        Ok(vec![FundsDeposited { amount: cmd.amount }.into()])
    }
}

Naming Conventions

Commands are imperative because they request an action:

GoodBad
DepositFundsDeposited
PlaceOrderOrderPlaced
RegisterUserUserRegistered
ChangePasswordPasswordChanged

Executing Commands

Use the repository to execute commands:

repository
    .execute_command::<Account, Deposit>(&account_id, &Deposit { amount: 100 }, &metadata)
    .await?;

The repository:

  1. Loads the aggregate from events
  2. Calls handle()
  3. Persists any resulting events
  4. Returns the result

Next

Projections — Building read models from events