CQRS Pattern
CQRS (Command Query Responsibility Segregation) separates the write model from the read model. Instead of using the same data structure for both creating/updating data and querying it, CQRS uses different models optimized for each purpose.
Why Separate Commands and Queries?
In traditional applications, a single model serves both purposes:
# Traditional: one model does everything
class User {
has $.id; has $.name; has $.email; has $.last-login;
method update-email($new-email) { ... } # Write
method get-display-name() { ... } # Read
}
This creates tension. Writes need normalization, constraints, and transactional consistency. Reads need denormalization, pre-computed aggregates, and query-specific shapes. Optimizing for one compromises the other.
The CQRS Solution
CQRS splits the model into two sides:
| Command Side (Write) | Query Side (Read) | |
|---|---|---|
| Purpose | Validate & process changes | Display & query data |
| Optimized for | Consistency, invariants | Query performance, flexibility |
| Consistency | Strong (within boundary) | Eventually consistent |
| How many | One per aggregate | Many, per use case |
| In Sourcing | Aggregations | Projections |
CQRS + Event Sourcing
When combined with Event Sourcing, CQRS becomes especially powerful:
Command arrives → Aggregation validates → Emits event
↓
Event Store
↓
┌─────────┴─────────┐
↓ ↓
Dashboard View Analytics View
Projection Projection
The aggregation (command side) validates the command against current state and emits an immutable event. The projections (query side) listen to the event stream and build their own optimized views.
How Sourcing Implements CQRS
Command Side — Aggregations
aggregation BankAccount {
has Int $.account-id is projection-id;
has Rat $.balance = 0;
method apply(AmountDeposited $e) { $!balance += $e.amount }
method apply(AmountWithdrawn $e) { $!balance -= $e.amount }
method withdraw(Rat $amount) is command {
die "Insufficient funds" if $!balance < $amount;
$.amount-withdrawn: :$amount;
}
}
The aggregation enforces the invariant (balance cannot go negative) before emitting any events.
Query Side — Projections
projection AccountSummary {
has Int $.account-id is projection-id;
has Rat $.balance = 0;
has Int $.transaction-count = 0;
method apply(AmountDeposited $e) {
$!balance += $e.amount;
$!transaction-count++;
}
method apply(AmountWithdrawn $e) {
$!balance -= $e.amount;
$!transaction-count++;
}
}
The projection builds a read-optimized view. It never validates or emits — it only observes and derives.
The same event stream feeds both the aggregation (for replay) and all projections. Adding a new projection doesn't require changing the aggregation or the event schema.
When to Use CQRS
CQRS adds complexity. Use it when:
- You need different read and write models (different data shapes, different performance requirements)
- You have complex business rules that need strong consistency
- You need multiple views of the same data (dashboard, reports, API responses)
- You need an audit trail of all changes
For simple CRUD applications, CQRS is overkill. Start with a single model and introduce CQRS when the tension between read and write requirements becomes painful.
Next Steps
- See how the pieces fit together in Architecture
- Learn about Aggregations — the command side
- Learn about Projections — the query side