Aggregations
An aggregation is the consistency boundary for business logic. It validates commands against current state and emits events that describe state changes. Aggregations are the command side (C) of CQRS.
What Aggregations Are
Aggregations are the gatekeepers of your domain. They answer: "Given what I know right now, is this command valid?" If yes, they emit an immutable event. If no, they reject the command.
- They are the only construct that can emit events
- They enforce invariants — business rules that must always be true
- One aggregate = one event stream = one transaction
- Internal state is private; external code interacts through command methods
Declaring an Aggregation
use Sourcing;
aggregation BankAccount {
has Int $.account-id is projection-id;
has Rat $.balance = 0;
has Bool $.is-open = False;
# State changes come ONLY from events
method apply(AccountOpened $e) {
$!balance = $e.initial-balance;
$!is-open = True;
}
method apply(AmountDeposited $e) { $!balance += $e.amount }
method apply(AmountWithdrawn $e) { $!balance -= $e.amount }
method apply(AccountClosed $e) { $!is-open = False }
# Command: validates before emitting
method withdraw(Rat $amount) is command {
die "Account is closed" unless $!is-open;
die "Amount must be positive" if $amount <= 0;
die "Insufficient funds: balance is $!balance" if $!balance < $amount;
$.amount-withdrawn: :$amount;
}
}
Key Characteristics
| Property | Description |
|---|---|
| Emits events | Aggregations are the only construct that can emit events. Events are immutable facts. |
| Enforces invariants | Business rules are enforced inside command methods. Invalid commands die before emitting. |
| One stream per instance | All events for a given aggregate form a single append-only stream. |
| State is encapsulated | Internal state is private. External code uses command methods and public attributes. |
| Source of truth | The event stream is the authoritative record. State can always be rebuilt from it. |
Command Methods
Methods marked with is command get special treatment:
- Reset + Replay — Before executing,
^updateresets the aggregate and replays all events from the store - Validation — The command body runs against fresh state
- Event emission — If validation passes, events are emitted with optimistic locking
- Automatic retry — If a concurrent modification is detected, the command retries (up to 5×)
method deposit(Rat $amount) is command {
die "Account is closed" unless $!is-open;
die "Amount must be positive" if $amount <= 0;
$.amount-deposited: :$amount; # Auto-generated emit method
}
Auto-Generated Emit Methods
For each event type handled by the aggregation, an emit method is auto-generated. The method name is the event class name converted to kebab-case:
| Event Class | Auto-Generated Method |
|---|---|
AmountDeposited | $.amount-deposited(:$amount) |
AccountOpened | $.account-opened(:$initial-balance) |
OrderPlaced | $.order-placed(:$customer, :$total) |
These methods automatically include the aggregation's projection ID values and handle optimistic locking.
Aggregate Design Rules
- Design around invariants. The boundary should enclose all data needed to enforce a business rule atomically.
- Keep aggregates small. A few hundred events max. Large aggregates become slow to replay.
- Name after domain concepts.
BankAccount, notAccountEntity. - One aggregate per transaction. Don't modify multiple aggregates atomically — use eventual consistency.
- Validate before emitting. Put all domain checks at the top of command methods.
- Never mutate state directly in commands. State changes happen only through event emission and
applymethods.
Don't mutate $!attributes directly inside command methods. This bypasses the event stream and breaks the fundamental guarantee of event sourcing. Always emit events and let apply methods handle state changes.
Next Steps
- Learn about Commands — the
is commandtrait and retry behavior - See the Bank Account Example for a complete aggregation
- Read the API Reference for Sourcing::Aggregation