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:
| Good | Bad |
|---|---|
Deposit | FundsDeposited |
PlaceOrder | OrderPlaced |
RegisterUser | UserRegistered |
ChangePassword | PasswordChanged |
Executing Commands
Use the repository to execute commands:
repository
.execute_command::<Account, Deposit>(&account_id, &Deposit { amount: 100 }, &metadata)
.await?;
The repository:
- Loads the aggregate from events
- Calls
handle() - Persists any resulting events
- Returns the result
Next
Projections — Building read models from events