Building Projections
A detailed guide to creating effective projections for your event-sourced application.
Basic Projection
Every projection needs at least one is projection-id attribute and apply methods:
projection UserView {
has Int $.user-id is projection-id;
has Str $.name;
has Str $.email;
method apply(UserCreated $e) {
$!name = $e.name;
$!email = $e.email;
}
method apply(UserNameChanged $e) {
$!name = $e.new-name;
}
}
Custom ID Mapping
When the event field name doesn't match your projection ID attribute:
projection OrderView {
has Int $.order-id is projection-id;
# Event has 'oid' field, map it to our 'order-id'
method apply(OrderCreated $e) is projection-id< oid > {
$!status = 'created';
}
}
Aggregating Data
Projections can compute derived values:
projection RevenueByMonth {
has Str $.month is projection-id; # e.g., "2024-03"
has Rat $.total = 0;
has Int $.order-count = 0;
has Rat $.average-order = 0;
method apply(OrderPlaced $e) {
$!total += $e.total;
$!order-count++;
$!average-order = $!total / $!order-count;
}
}
Tracking Collections
Projections can maintain lists of related data:
projection CustomerOrderHistory {
has Int $.customer-id is projection-id;
has Array @.orders;
method apply(OrderPlaced $e) {
@!orders.push: {
order-id => $e.order-id,
total => $e.total,
date => $e.date,
};
}
}
Best Practices
1. One Projection Per Query Purpose
Don't build a "god projection." Each projection should serve one specific query:
# Good: focused projections
projection UserDashboard { ... }
projection UserAdminView { ... }
projection UserAuditLog { ... }
# Bad: one projection trying to do everything
projection UserEverything {
has $.dashboard-data;
has $.admin-data;
has $.audit-log;
# ...
}
2. Make Apply Methods Idempotent
Applying the same event twice should produce the same state:
# Good: idempotent
method apply(UserNameChanged $e) {
$!name = $e.new-name; # Same result every time
}
# Bad: not idempotent
method apply(UserNameChanged $e) {
$!name ~= $e.new-name; # Duplicates on replay!
}
3. Name Projections After Their Purpose
# Good
projection CustomerDashboardView
projection MonthlyRevenueReport
projection FraudDetectionAlert
# Bad
projection CustomerEventProjection
projection OrderProjection
4. Don't Enforce Constraints
Constraints belong in aggregations. If a projection shows unexpected data, it's a reporting concern:
# Good: just reflect the events
method apply(InventoryAdjusted $e) {
$!quantity += $e.delta;
}
# Bad: trying to enforce rules
method apply(InventoryAdjusted $e) {
die "Quantity can't be negative" if $!quantity + $e.delta < 0;
$!quantity += $e.delta;
}
Next Steps
- See the Bank Account Example for multiple projections from the same events
- Learn about Sourcing::Projection API for detailed reference