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)
PurposeValidate & process changesDisplay & query data
Optimized forConsistency, invariantsQuery performance, flexibility
ConsistencyStrong (within boundary)Eventually consistent
How manyOne per aggregateMany, per use case
In SourcingAggregationsProjections

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.

Key Insight

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:

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