Commands

Commands are the write entry points for aggregations. They provide a safe place to validate business rules against the current aggregate state before emitting events that become immutable facts.

Why Commands Exist

Every command passes through two validation gates:

  1. Superficial validation — Required fields, format checks, valid ranges. Happens before the command reaches the domain layer (e.g., at the API boundary).
  2. Domain validation — Business rules that depend on aggregate state. Happens inside the command method. Examples: "withdrawal cannot exceed balance", "order cannot be cancelled after shipping".

Commands are the right place for domain validation because they run against the most recent state of the aggregation.

The Command Lifecycle

Command called
    │
    ▼
┌─────────────────────────────────┐
│ 1. ^update: Reset + Replay      │
│    • Reset all mutable attrs    │
│      to their defaults          │
│    • Replay ALL events from     │
│      the store onto the agg     │
│    • Aggregate is now fresh     │
└──────────────┬──────────────────┘
               │
               ▼
┌─────────────────────────────────┐
│ 2. Execute command body         │
│    • Validate against state     │
│    • die() if validation fails  │
│    • Emit events via $.event    │
└──────────────┬──────────────────┘
               │
               ▼
┌─────────────────────────────────┐
│ 3. Optimistic lock check        │
│    • Store detects if new       │
│      events appeared since      │
│      the ^update call           │
│    • Throws X::OptimisticLocked │
│      if contention detected     │
└──────────────┬──────────────────┘
               │
        ┌──────┴──────┐
        │             │
   Success         Contention
        │             │
        ▼             ▼
   Return result   Retry (up to 5x)
                   Go to step 1
                   │
                   ▼ (all retries exhausted)
             Throw X::OptimisticLocked

Automatic Retry

When X::OptimisticLocked is thrown, the command automatically retries — up to 5 attempts. This works because:

After 5 failed attempts, the last X::OptimisticLocked exception is re-thrown to the caller.

Command Patterns

Simple Command

method increment(Int $amount) is command {
    $.incremented: :$amount;
}

Command with Validation

method withdraw(Rat $amount) is command {
    die "Account is closed" unless $!is-open;
    die "Insufficient funds: $!balance < $amount" if $!balance < $amount;
    $.amount-withdrawn: :$amount;
}

Command Emitting Multiple Events

method place-order() is command {
    die "Cannot place empty order" if @!items.elems == 0;
    $.order-placed;
    $.payment-requested: total => @!items.map(*.price).sum;
}

Best Practices

Commands Are Not CRUD Updates

A CRUD UPDATE users SET balance = balance - 100 mutates state directly. A command validates against current state, then emits an event that describes the change. The event is the source of truth, not the resulting state.

Next Steps