CQRS Boundary Criteria: When Separate Models Finally Earn Complexity Tax

CQRS Boundary Criteria: When Separate Models Finally Earn Complexity Tax
9 views
CQRS is not a badge — it is an operational bill. Dual models mean dual migrations, dual failure modes, and eventual consistency bugs that support tickets will call “random.” Seniors introduce separate read models only when measured query pain exceeds that tax, with explicit lag SLOs and a rollback story.
⚡ TL;DR: Split when write-optimized schema cannot meet p95 read latency or fan-out joins without melting writers. Keep a single model until then. Measure lag, dual-deploy cost, and consumer breakage. Pair with Read-Your-Writes on Global Tables and DynamoDB Streams Outbox.

CQRS Boundary Criteria: production guidance

python
# cqrs/decision.py
from dataclasses import dataclass

@dataclass
class CqrsSignals:
 write_p95_ms: float
 read_p95_ms: float
 read_qps: float
 join_depth: int
 projection_lag_slo_ms: float
 team_can_own_two_schemas: bool

def should_split(s: CqrsSignals) -> tuple[bool, str]:
 if not s.team_can_own_two_schemas:
 return False, "no_owner_for_projection"
 if s.read_p95_ms < 80 and s.join_depth <= 2:
 return False, "single_model_still_fast"
 if s.write_p95_ms > 100 and s.read_qps > 500 and s.join_depth >= 3:
 return True, "write_path_contends_with_heavy_reads"
 if s.projection_lag_slo_ms < 200 and s.read_qps > 2000:
 return True, "hot_read_path_needs_denorm"
 return False, "tax_not_justified"
❌ Splitting because a conference talk said “commands and queries must diverge” — that is fashion, not criteria.

Boundary patterns that earn their keep

Pattern When it pays Hidden cost
Same DB, denormalized read tables Heavy joins on OLTP Trigger/CDC dual-write bugs
Event-sourced writes + projection Audit + many read shapes Replay tooling, lag SLOs
Separate store (OpenSearch / Dynamo) Search / fan-out reads Schema drift, reindex jobs
Per-tenant read replicas Isolation Failover and lag UX

Projection contract with lag SLO

typescript
// cqrs/projection.ts
export async function projectOrderPlaced(evt: OrderPlaced) {
 await readDb.upsert({
 pk: `order#${evt.orderId}`,
 customerId: evt.customerId,
 total: evt.total,
 status: "placed",
 version: evt.version, // ✅ monotonic — reject older
 projectedAt: Date.now(),
 });
}

export async function assertLag(orderId: string, writeAt: number, sloMs = 500) {
 const row = await readDb.get(`order#${orderId}`);
 if (!row || row.projectedAt - writeAt > sloMs) {
 metrics.hit("ProjectionLagBreach");
 // ✅ Fall back to write-side get for read-your-writes sessions
 return "fallback_write_model";
 }
 return "ok";
}
UI paths that just created an entity must tolerate lag — sticky sessions to the write model or wait-for-projection tokens beat lying to the user.

Operational gate: dual deploy tax

Before merging the split, answer:
  1. Who owns projection failures on-call?
  2. Can you rebuild the read model from events in < 4 hours?
  3. Do contract tests fail the PR when event fields drop?
  4. Is there a feature flag to serve reads from the write model?
If any answer is vague, keep one model and add covering indexes.

Closing checklist

✅ Dos – ✅ Score latency, join depth, and ownership before splitting – ✅ Publish projection lag SLOs and breach metrics – ✅ Version projections; reject stale events – ✅ Provide read-your-writes fallback for create flows – ✅ Prove rebuild time from the event log ❌ Don’ts – ❌ Don’t CQRS every CRUD microservice by default – ❌ Don’t ignore dual-schema migration cost – ❌ Don’t expose lag as “eventual” without UX handling – ❌ Don’t let projections silently drop fields – ❌ Don’t split when an index or materialized view would do

Related reading

Last updated on September 11, 2026 Related: Multi-Agent Systems: Orchestrating Claude, GPT-4o, and Specialized Models Together; Multi-Model Routing: Cheap Draft Models Plus Expensive Verifiers; CI Failure Triage Bots: Separate Flaky Noise From Real Regressions.

Watch: 100 Days of AI on YouTube

Short videos from CheatCoders, one AI engineering topic per day.

Open the playlist on YouTube · CheatCoders on YouTube

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

1 Comment

Leave a comment

No account needed. Name and email are optional.