← CodeClarityLab Home
Browse by Category
+ added · updated 7d
← Back to glossary

CQRS (Command Query Responsibility Segregation)

architecture PHP 7.0+ Advanced
debt(d7/e7/b7/t5)
d7 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'only careful code review or runtime testing' (d7). The detection_hints explicitly state automated detection is 'no'. Detecting CQRS misuse (same model for complex reads and writes, impedance mismatch) requires careful code review to identify when read/write models should be separated. No static analysis tool can determine if your domain complexity justifies CQRS or if you're over-engineering simple CRUD.

e7 Effort Remediation debt — work required to fix once spotted

Closest to 'cross-cutting refactor across the codebase' (e7). Introducing or fixing CQRS misuse requires separating read and write models throughout the system — creating separate query classes, potentially restructuring repositories, and updating all consumers. The quick_fix mentions 'start simple with separate query classes' but retrofitting CQRS into an existing codebase where models are shared touches many files and requires coordinated changes across command handlers, query handlers, and their consumers.

b7 Burden Structural debt — long-term weight of choosing wrong

Closest to 'strong gravitational pull' (b7). CQRS is an architectural pattern that applies across web, api, and cli contexts. Once adopted, every new feature must consider whether it's a command or query, maintain separate models, and respect the separation. The common_mistakes note that sharing write model objects with the read side 'defeats the purpose' — the pattern demands ongoing discipline. Every future developer must understand and follow the separation, shaping how all data access is designed.

t5 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'notable trap' (t5). The misconception field states developers wrongly believe 'CQRS requires separate databases for reads and writes' when actually a single database with two code paths is valid CQRS. This is a documented gotcha that most developers eventually learn, but initially misleads teams into thinking they need complex infrastructure. The common_mistakes reinforce this: applying CQRS to simple CRUD, expecting immediate consistency, and implementing without event sourcing are all predictable traps.

About DEBT scoring →

Also Known As

Command Query Responsibility Segregation CQRS pattern

TL;DR

Separates the model for reading data (queries) from the model for writing data (commands), enabling independent optimisation of each.

Explanation

CQRS (Greg Young) extends Command Query Separation from the method level to the architectural level — different models, and potentially different data stores, handle reads and writes. The write model enforces business rules and emits domain events; the read model is a denormalised, query-optimised projection updated asynchronously. This decoupling allows reads to scale independently of writes, enables event sourcing, and keeps the write model focused on invariants. CQRS adds significant complexity — only apply it where read/write asymmetry genuinely exists.

Diagram

flowchart LR
    subgraph Write Side
        CMD[Command] --> CH[Command Handler]
        CH --> DOM[Domain Model]
        DOM --> ES[(Event Store)]
    end
    subgraph Read Side
        ES -->|Project| RM[(Read Model)]
        QRY[Query] --> QH[Query Handler]
        QH --> RM
    end
    UI[Client] --> CMD
    UI --> QRY
style ES fill:#6e40c9,color:#fff
style RM fill:#238636,color:#fff
style UI fill:#1f6feb,color:#fff

Common Misconception

CQRS requires separate databases for reads and writes. CQRS is a principle — separating command (write) and query (read) models. A single database with two code paths is valid CQRS. Separate read stores are an optional optimisation, not a requirement.

Why It Matters

CQRS separates the model for writing data from the model for reading it — writes go through a rich domain model that enforces business rules, reads use a flat, optimised projection. This makes both sides dramatically simpler and allows independent scaling.

Common Mistakes

  • Applying CQRS to simple CRUD screens — the complexity is only justified when read and write models genuinely differ.
  • Expecting read models to be immediately consistent — CQRS often pairs with eventual consistency, and UIs need to handle that.
  • Sharing the write model's domain objects with the read side — that defeats the purpose of separation.
  • Implementing CQRS without event sourcing and wondering why read model rebuilding is painful.

Avoid When

  • Simple applications where read and write models are identical — CQRS doubles the surface area with no benefit.
  • Small teams that cannot maintain two separate models, sync mechanisms, and consistency guarantees.
  • Latency-sensitive operations where eventual consistency between command and query sides is unacceptable.
  • Greenfield projects — start simple and introduce CQRS only when read/write scaling demands diverge.

When To Use

  • High-read, low-write systems where optimised read models (denormalised, cached projections) dramatically improve performance.
  • Complex domains where the write model (aggregates, invariants) and read model (flat views, reports) have fundamentally different shapes.
  • Event-sourced systems where read models are built by replaying the event stream.
  • Systems requiring different scaling strategies for reads vs writes — e.g. read replicas, CDN-cached views.

Code Examples

✗ Vulnerable
// No CQRS — read and write mixed in one model:
class OrderService {
    public function getOrder(int $id): Order { return Order::with('items')->find($id); }
    public function placeOrder(array $data): Order { /* validate, create, dispatch events */ }
    // Reads and writes share the same model — can't optimise independently
}
✓ Fixed
// COMMAND — write side, returns void
class PlaceOrderCommand {
    public function __construct(public readonly CartId $cartId, public readonly UserId $userId) {}
}
class PlaceOrderHandler {
    public function handle(PlaceOrderCommand $cmd): void {
        $cart  = $this->carts->find($cmd->cartId);
        $order = Order::place($cart, $cmd->userId);
        $this->orders->save($order);
    }
}

// QUERY — read side, returns a DTO, never mutates
class OrderSummaryQuery {
    public function __construct(public readonly UserId $userId) {}
}
class OrderSummaryHandler {
    public function handle(OrderSummaryQuery $q): array {
        return $this->db->query('SELECT id,total FROM orders WHERE user_id=?', [$q->userId]);
    }
}

Added 15 Mar 2026
Edited 25 Mar 2026
Views 28
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings F 2 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 1 ping S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 2 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 1 ping S 1 ping S 1 ping M 0 pings T 0 pings W 0 pings T 0 pings F 1 ping S
No pings yesterday
Amazonbot 8 Perplexity 7 Ahrefs 4 Unknown AI 2 Google 1
crawler 22
DEV INTEL Tools & Severity
🔵 Info ⚙ Fix effort: High
⚡ Quick Fix
Separate read models (optimised for query) from write models (enforcing business rules) — start simple with separate query classes before full CQRS
📦 Applies To
PHP 7.0+ web api cli
🔗 Prerequisites
🔍 Detection Hints
Same model used for complex writes and complex reads causing impedance mismatch; reports joining many tables to reconstruct read view
Auto-detectable: ✗ No
⚠ Related Problems
🤖 AI Agent
Confidence: Low False Positives: High ✗ Manual fix Fix: High Context: File Tests: Update

✓ schema.org compliant