Welcome to Grind Engineer , your guide to becoming a better engineer!
No fluff. Pure engineering insights.

The best candidate for your next role might not live in the same country. Oyster helps you hire globally in 180+ countries. Payroll, compliance, and benefits included.

Stop typing what you could say in 10 seconds.

Wispr Flow turns your voice into clean, professional text inside any app. Emails, Slack, client updates — speak once, send without editing. 4x faster than typing.

Nobody wants to say no to their team. Ramp does it for you with AI-powered cards that automatically block out-of-policy spend. Let Ramp be the bad guy.

Most applications start the same way. One API, one database, one model that handles both reading and writing data. You build a users table, and it serves "create a new user" and "show user profile" with the same schema, same connection pool, same scaling strategy. For a while, that's perfectly fine.

Then your product grows. And something subtle starts breaking.

Your product listing page needs to load in under 200ms, but your order processing requires strict transactional consistency. Your analytics dashboard wants to query across five different entities in a single view, but your write model is normalized across five different tables. Your reads outnumber your writes 10 to 1, but you can't scale them independently because they share the same database.

That tension reads wanting speed and flexibility, writes wanting integrity and validation is the exact problem CQRS solves. And companies like Netflix, Uber, and Amazon use it in production every day.

TL;DR: CQRS (Command Query Responsibility Segregation) splits your application into two separate models — one for writing data (commands) and one for reading data (queries). Each model gets its own database, schema, and scaling strategy. This article covers what it is, why it exists, when to use it, when NOT to use it, and how it connects to event sourcing.

Why a Single Model Breaks Down

Every CRUD application starts with a single data model. Your Order table handles both "place an order" and "show order history." Your Product table handles both "update inventory" and "display product catalog." Same schema, same database, same everything.

CQRS Overview

The problem is that reading data and writing data have fundamentally different needs, and a single model forces you to compromise on both.

When you write data, you care about integrity. You want normalized schemas so there's no data duplication. You want foreign key constraints so relationships stay valid. You want transactions so partial writes never happen. You want your schema designed around how data relates to itself, not how it gets displayed.

When you read data, you care about speed and shape. You want denormalized views so a single query returns everything the UI needs without expensive JOINs. You want precomputed aggregations so dashboards load instantly. You want your data shaped around what the consumer actually needs to see, not how it's stored internally.

A normalized schema optimized for writes forces your reads to do expensive multi table JOINs on every page load. A denormalized schema optimized for reads creates data duplication that makes writes complicated and error prone. You can't have both in the same model without one side paying the price.

This is the fundamental insight behind CQRS: stop trying to use one model for two different jobs.

What CQRS Actually Is

CQRS stands for Command Query Responsibility Segregation. The name is more intimidating than the idea. It breaks your application into two separate paths.

Commands are any operation that changes state. Creating a user, placing an order, updating inventory, deleting a record. Commands don't return data. They return success or failure. Their job is to validate business rules and persist changes correctly.

Queries are any operation that reads state. Loading a product page, fetching order history, rendering a dashboard. Queries never modify data. They return exactly the shape the caller needs, nothing more, nothing less.

Responsibility Segregation means these two concerns get completely separate models. Separate schemas. Often separate databases. Separate scaling strategies. Separate optimization goals.

The command side uses a normalized relational database — PostgreSQL, MySQL — optimized for data integrity, foreign key enforcement, and transactional consistency. It doesn't care about read performance at all. Its only job is to make sure writes are correct.

The query side uses a denormalized data store — Redis, Elasticsearch, DynamoDB, or even a separate PostgreSQL schema with precomputed views — optimized purely for read speed. Every query hits a flat, prejoined view that returns exactly what the caller needs. Zero JOINs, zero computation at query time, sub millisecond latency.

The result is that you can optimize each side independently. Your write database can enforce every constraint, run every validation rule, and take as long as it needs to ensure correctness. Your read database can denormalize everything into instant lookup tables without worrying about data integrity, because that's not its job.

A Concrete Example

Say you run an e-commerce store and a customer wants to see their order history. Without CQRS, every page load runs a query that JOINs across your orders table, users table, order items table, and products table. That's four tables joined together on every single request. Under load, this query becomes the bottleneck — and you can't optimize it without changing the schema that your writes depend on.

With CQRS, the write side processes the order into normalized tables with full transactional integrity. Then an event triggers the read side to update a flat, precomputed order_history_view that has everything pre joined — user name, product titles, quantities, prices, status — all in a single row. When the customer loads their order history page, it hits one table with zero JOINs. Read latency drops from 120ms to 3ms.

The key insight is that the write happened once, but that order history page might get loaded hundreds of times. You do the expensive work of joining and computing once during the write, and every subsequent read is essentially free.

How the Two Sides Stay in Sync

The biggest question engineers ask about CQRS: if reads and writes use different databases, how do they stay consistent?

The most common approach in production is asynchronous events. After a write completes, the command side publishes an event to a message bus — Kafka, RabbitMQ, AWS SNS. A consumer picks up that event and updates the read model accordingly. This is how Netflix, Uber, and most large scale systems implement CQRS.

The tradeoff is eventual consistency. There's a brief window — usually milliseconds, sometimes a few seconds — where the read model hasn't caught up to the latest write. If a user places an order and immediately checks their order history, they might not see it for a fraction of a second.

For most applications, this is completely acceptable. Users already experience this in everyday apps. When you post on social media, your post doesn't appear in everyone's feed at the exact same millisecond. When you transfer money between bank accounts, the balance doesn't update simultaneously across every screen. The world already runs on eventual consistency; most people just don't realize it.

If you need stronger consistency, you can use synchronous projection — updating the read model inside the same transaction as the write. This eliminates the stale window but couples your read and write performance together. If the read database is slow, writes slow down too. This works for smaller systems but defeats the purpose of separating the models at scale.

A third option is event sourcing, where instead of storing current state you store every event that ever happened. The read model is rebuilt by replaying events. This gives you a complete audit trail and the ability to reconstruct any view from scratch, but it adds significant complexity. More on this below.

When CQRS Is the Right Call

CQRS adds real complexity to your system. Two databases, a synchronization mechanism, eventual consistency to reason about, more infrastructure to operate. You need a concrete reason to justify that cost.

The clearest signal is when reads massively outnumber writes — 10 to 1 or more. If 90% of your traffic is reading data and only 10% is writing it, you're forcing your write optimized schema to serve a workload it was never designed for. Separating the models lets you scale the read side independently by adding read replicas, caching layers, or faster query databases without touching the write infrastructure at all.

Another strong signal is when your read and write schemas need to look fundamentally different. Your write side might need a normalized schema across five tables to maintain referential integrity. But your API consumers need a single flat object with everything pre joined. Instead of forcing one schema to serve both masters, you give each side the schema it actually needs.

CQRS also makes sense when you need multiple read views from the same data. A product page, an admin dashboard, a search index, and an analytics pipeline all need different shapes of the same underlying data. With a single model, every new view either adds complexity to your queries or requires schema changes. With CQRS, you add a new projection that consumes the same events and builds the view it needs. The write side doesn't change at all.

When NOT to Use CQRS

This is the part most articles skip, and it matters more than the rest.

Don't use CQRS for simple CRUD applications. If your reads and writes are roughly balanced, your queries are fast enough, and your schema serves both purposes well, you're adding two databases and a message bus for zero benefit. Most applications never reach the scale where a single database becomes the bottleneck.

Don't use CQRS when your team is small. Two or three engineers maintaining two data models, a message bus, event consumers, and the eventual consistency edge cases is a lot of operational overhead. The complexity cost outweighs the performance gain until your team and traffic both justify it.

Don't use CQRS for your MVP. Ship with one database. Measure your read/write ratio. Profile your slow queries. If reads dominate 10 to 1 and your read latency is suffering despite query optimization, THEN consider CQRS. Premature model separation is worse than a slow query you can optimize with an index.

Don't use CQRS when strong consistency is a hard requirement everywhere. If stale reads are never acceptable — say, showing a bank balance right before a withdrawal — the async sync model introduces problems. You can work around them, but each workaround adds complexity that might not be worth it.

Martin Fowler puts it directly: "For some situations, having separate models for reads and writes can be valuable, but for most situations, using CQRS can add risky complexity."

You'll often see CQRS mentioned alongside event sourcing, and many engineers assume they're the same thing. They're not. They solve different problems, and you can use either one without the other.

Without event sourcing, your database stores the current state. An order has status: shipped. You know where it is right now, but you don't know the full sequence of changes that got it there. If something goes wrong, you're debugging from a snapshot with no history.

With event sourcing, you store every event that ever happened: OrderPlaced, PaymentProcessed, ItemPacked, OrderShipped. The current state is derived by replaying these events in order. You get a complete audit trail for free, and you can reconstruct the state of any entity at any point in time by replaying events up to that moment.

The combination with CQRS is powerful. Events from the write side flow into an event log — Kafka, EventStoreDB — and projections consume those events to build read optimized views. If a read model gets corrupted or you need an entirely new view, you replay the events from scratch and rebuild it. You never lose data because the event log is the ultimate source of truth.

But event sourcing adds significant complexity. You need to handle event versioning as your schemas evolve. Storage costs grow because you're keeping every event forever. Debugging requires understanding event replay mechanics. For most teams, CQRS without event sourcing is the right starting point. You get 80% of the benefit — independent scaling, optimized schemas, multiple read views — without the complexity of storing and replaying events. Add event sourcing later if you need audit trails or the ability to rebuild read models from scratch.

Real World CQRS at Scale

Company

How They Use CQRS

Netflix

Content finance services publish events (content licensed, royalty calculated) to Kafka topics. Downstream consumers build read optimized views for dashboards and reporting. Write and read sides scale independently across 260 million subscribers.

Uber

Pricing, ride, and fraud detection events flow through Kafka and Flink. Read optimized views power the rider app, driver app, and internal analytics with sub second query latency. The write side ensures exactly once semantics independently.

Amazon

Order fulfillment validates inventory, processes payments, and records transactions on the write side. Separate read models power order history, package tracking, and seller dashboards — each denormalized for its specific audience.

The common thread: all three companies hit the exact same wall. Reads and writes needed different things. One model couldn't serve both. CQRS let each side optimize independently.

What This Means For Engineers

Start with one database. Seriously. Measure your read/write ratio. Profile your slow queries. Most of the time, an index, a materialized view, or a caching layer solves your read performance problem without the complexity of a full model split.

When you do need CQRS, start without event sourcing. Separate your read and write models. Use Kafka or SQS for async sync. Accept eventual consistency. This gets you independent scaling and optimized schemas with manageable complexity.

Design your UI for eventual consistency from day one. Even without CQRS, network latency means your UI is already showing stale data. Show optimistic updates ("Order placed!") immediately and reconcile when the backend catches up. This pattern serves you whether you adopt CQRS or not.

The pattern is simple. The judgment call is knowing when your system actually needs it versus when a well placed index would have done the job.

That’s it for today, keep learning!
Scortier, Signing Off!

Subscribe to keep reading

This content is free, but you must be subscribed to Grind Engineer to continue reading.

Already a subscriber?Sign in.Not now

Reply

Avatar

or to participate

Keep Reading