Deciding is where your domain says yes or no. A command comes in; the
aggregate reads its current state, checks its invariants, and either produces
events (yes) or returns a domain error (no). In mnesis that decision is the
Handle<C> trait — one impl per command type.
pub trait Handle<C, const N: usize = 0>: Aggregate {
fn handle(state: &Self::State, cmd: C) -> Result<Events<EventOf<Self>, N>, Self::Error>;
}Read the signature closely — every part is deliberate:
state: &Self::State— a borrow of the current state. The decision reads it; it cannot mutate it. Producing events is the only way to change anything.cmd: C— the command, taken by value (the caller constructed it fresh).- No
self, no version, no id. A decision is a pure function of(state, command)only. It never branches on persistence position — "am I at version 7?" is never a valid reason to decide differently. This is enforced by the signature: the handler simply cannot see the version. Result<Events<..>, Self::Error>— yes returns at least one event; no returns a typed domain error.
Handlers are implemented on the marker type, not on AggregateRoot:
impl Handle<Withdraw> for BankAccount {
fn handle(state: &AccountState, cmd: Withdraw) -> Result<Events<AccountEvent>, AccountError> {
if !state.is_open { return Err(AccountError::Closed); }
if state.balance < cmd.amount {
return Err(AccountError::InsufficientFunds { balance: state.balance, amount: cmd.amount });
}
Ok(events![AccountEvent::Withdrawn(MoneyWithdrawn { amount: cmd.amount })])
}
}handle decides; it does not change the aggregate. The returned events are a
proposal. They become real state only after they are persisted and folded in
(via commit_persisted, usually inside a repository
save). This separation is the whole point:
handle(state, cmd) ─► Events (decide: can fail, mutates nothing)
│
persist events (durability)
│
commit_persisted(v, &events) (apply: infallible fold, advances version)Validation lives here, in handle — never in AggregateState::apply, which is
infallible because an event is already a fact. Your invariants have exactly one
home.
A successful decision returns Events<E, N>: an ArrayVec-backed collection that
guarantees at least one event and has a compile-time capacity of N + 1. The
default N = 0 means "exactly one event" — the common case. Either way the
ArrayVec lives on the stack, so there is no heap allocation for any N, which
keeps it no_std-friendly. Emitting two events? Declare Handle<C, 1> and build with
events![a, b].
impl Handle<CloseAccount, 1> for BankAccount {
fn handle(state: &AccountState, _: CloseAccount) -> Result<Events<AccountEvent, 1>, AccountError> {
// ... may emit up to two events ...
}
}One
Handle<C, N>per command type. Two impls that differ only inNfor the sameCare ambiguous at the dispatch site — pick the capacity the command actually needs.
On a loaded aggregate, AggregateRoot::handle forwards to the matching impl and
infers everything:
let decided = account.handle(Withdraw { amount: 300 })?;- Aggregates — the state and container the decision reads.
- Repository — persist decided events and fold them back in.
- Sagas —
React, the event-driven dual ofHandle.