CQRS (Command Query Responsibility Segregation)
debt(d7/e7/b7/t5)
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.
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.
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.
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.
Also Known As
TL;DR
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
Why It Matters
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
// 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
}
// 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]);
}
}