CQRS: The Pattern CTOs Misapply Most
Meta: "CQRS solves a specific scaling problem. Most startups apply it too early here's the honest decision framework for when it earns its complexity."
Y
our team shipped CQRS three months ago. Two separate models one for writes, one for reads. Separate databases. An event bus in the middle keeping them in sync. The engineers are proud of it. The architecture diagram looks sophisticated.And your feature velocity has dropped by half.
CQRS (Command Query Responsibility Segregation) is a legitimate pattern that solves a real problem. It's also one of the most consistently misapplied patterns in startup engineering. Not because engineers misunderstand how it works, but because they apply it before they have the problem it's designed to solve. That's the mistake this article is about.
What CQRS Actually Is
At its core, CQRS is one idea: separate the model you use to write data from the model you use to read it.
That's it. The rest event sourcing, separate databases, message queues, eventual consistency are possible implementations of that idea, not requirements of the pattern itself.
The original formulation by Greg Young is narrower than most people think. A command changes state and returns nothing. A query returns data and changes nothing. The implication is that you can optimize each side independently, because they have fundamentally different shapes. Writes need strong consistency and business rule enforcement. Reads need flexibility, speed, and shapes that fit what UIs actually render.
In a standard CRUD application, one model tries to serve both. The database schema is a compromise between write efficiency and read efficiency. When that compromise starts to hurt when your read queries are joining 11 tables and still returning too slowly, while your write transactions are blocked because those same tables are locked CQRS gives you an exit.
The Problem It Actually Solves
CQRS is a scaling pattern for read/write asymmetry.
Most applications read far more than they write. A fintech dashboard might write a transaction once and read it fifty times in the transaction history view, the balance calculation, the export, the reconciliation report, the fraud review. Each of those reads has a different shape, different sorting, different aggregation. Forcing all of them through the same normalized write model creates either slow reads or a messy schema that's trying to be everything.
The read side of CQRS lets you maintain purpose-built read models denormalized projections of the write-side data, shaped for specific query patterns. The balance view has its own table. The reconciliation report has its own materialized projection. Each is fast because it's built for exactly one purpose.
The write side stays clean. It enforces business rules, validates invariants, writes to the canonical store. It doesn't care how the read side is structured.
This is genuinely useful. At the scale where it becomes useful, it's often worth the complexity. The question is whether you're at that scale.
Where Teams Apply It Too Early
The pattern gets misapplied in one of two ways.
The first: applying it to the whole system at greenfield. A seed-stage startup with 200 users builds full CQRS infrastructure on day one because the CTO read about it on a Gojek engineering post. Now they have two databases to manage, an event bus to operate, and eventual consistency bugs to debug for a product that could have served its current load with a single Postgres instance and three well-written queries.
The second: applying CQRS to parts of the system that don't have read/write asymmetry. An admin panel that's written to and read from equally. An internal tool used by five people. A domain where the write model and the read model are essentially the same shape. CQRS adds overhead here with no benefit.
The honest test: if you can write a SELECT query that returns what you need in under 50ms with a reasonable index, you don't need CQRS for that path. If your read queries are becoming materially more complex than your write logic different joins, different aggregations, fundamentally different structure that's the signal.
The Part Most People Get Wrong
The most common misconception: CQRS requires event sourcing.
It doesn't. They're often paired, and the combination is powerful, but they're independent. You can implement CQRS with a simple approach: write to your primary Postgres database, then use a background process to project that data into read-optimized tables or a separate read replica. No event bus. No Kafka. No event store.
This simpler form sometimes called "CQRS-lite" is often the right starting point. It gets you the main benefit (purpose-built read models) with a fraction of the operational complexity.
The fuller CQRS + event sourcing combination makes sense when you also need a complete audit history, time-travel queries, or the ability to replay events to build new projections. Indonesian fintech companies building transaction ledgers are a legitimate use case for this the regulatory requirement for immutable audit trails aligns well with event sourcing's append-only model. For an e-commerce product catalog, it's overkill.
The second misconception: CQRS guarantees consistency. It doesn't it trades strong consistency for scalability. The read side is eventually consistent with the write side. If a user submits a payment and immediately refreshes their transaction history, they might not see it yet. Your product has to be designed around that reality. Some domains tolerate it fine. Others anything where a user expects to see their own write reflected immediately require careful UX design or read-your-writes consistency tricks that add their own complexity.
Real-World Example
A payments platform in Jakarta ran into a concrete version of this. Their transaction ledger was a single Postgres table, heavily normalized. Reads for the customer dashboard required six joins and a window function. At 50k transactions a day this was fine. At 500k, dashboard queries were regularly taking 3–4 seconds. The business needed them under 500ms.
They evaluated full CQRS with Kafka and a separate read store. The operational cost was significant: Kafka cluster to manage, consumer lag to monitor, a new failure mode where the read side falls behind and customers see stale data.
Instead they implemented CQRS-lite. The write path stayed unchanged Postgres, normalized schema, strong consistency. A background worker (a simple Go service polling a transactions changelog table via logical replication) projected transaction data into two read-optimized tables: one shaped for the customer dashboard, one for the reconciliation export. Both were denormalized, pre-aggregated, indexed for their specific query pattern.
Dashboard queries dropped to 40ms. The projection worker adds about 200ms of lag which the product team accepted because the dashboard already had a "refreshing..." state. No Kafka. No event store. Postgres-to-Postgres, one background worker, two read tables.
That's where CQRS earns its keep: a specific, measured pain point, solved with the minimum viable implementation of the pattern.
FAQ
Q: How do I know if my system is ready for CQRS? A: You have a specific, measurable read performance problem that you can't solve with better indexing or query optimization. Your read model and write model have genuinely diverged the data shape for reads is materially different from the shape you write. You have the operational capacity to manage eventual consistency in your product and in your infrastructure.
Q: Should CQRS and event sourcing always be used together? A: No. CQRS-lite projecting from a primary write store into read-optimized tables without a full event store solves most of the performance problems with a fraction of the complexity. Add event sourcing only when you need the audit trail, the ability to replay history, or the ability to build new projections from historical events. These are real requirements in regulated industries like fintech.
Q: What's the operational cost of full CQRS in production? A: Significant. You're managing two data stores, a projection mechanism (queue, change data capture, or event store), consumer lag monitoring, and eventual consistency edge cases in your product. If your team is smaller than 5–6 engineers, that operational overhead will slow you down more than the read performance problem was.
Q: How does CQRS interact with a legacy rewrite? A: It's a useful transitional pattern. During a strangler fig migration, you can introduce CQRS read models that pull from both the legacy and new write paths, giving you a consistent read layer while the underlying write system transitions. The Anti-Corruption Layer pattern typically sits alongside this to translate between legacy and new domain models.
Q: Is CQRS worth it for an Indonesian startup at Seed stage? A: Almost certainly not yet. At Seed, your read/write volumes are low, your domain model is still changing rapidly, and the operational overhead of full CQRS will slow your iteration speed. Build a clean, well-indexed Postgres schema. Write queries that are efficient. When a specific query path becomes a measurable problem and you'll know when it does consider CQRS-lite for that path only. Don't boil the ocean.
CQRS isn't a bad pattern. It's a specific tool for a specific problem that tends to get reach for too early, applied too broadly, and implemented in its most complex form when a simpler one would do.
The teams that get it right start with the problem, not the pattern and they implement exactly as much of it as the problem requires.
Internal links used:
- strangler fig migration CQRS as a transitional pattern during legacy rewrites
- Anti-Corruption Layer pattern co-located concern in legacy migration
External links used:
- None
Word count: 1,520