Home
ArenaGraphSignalTopics
Back to Feed

Command Query Responsibility Segregation (CQRS): Splitting Reads and Writes

Last Updated • 10d ago

Command Query Responsibility Segregation (CQRS): Splitting Reads and Writes

In the lifecycle of a fast-growing application, you eventually hit a wall where optimizing a single database for both read-heavy traffic and complex write transactions becomes physically impossible.

Imagine a social network. The platform processes 10,000 writes per second (likes, comments, posts) and 1,000,000 reads per second (timeline fetches, profile views).

If you use a single, highly normalized SQL database:

  • Writes are reasonably fast because normalization prevents data duplication, but they take exclusive locks that block reads.
  • Reads are agonizingly slow because fetching a user's timeline requires a JOIN across the Users, Posts, Comments, and Likes tables while dodging write locks.

You try adding indexes to speed up the reads. But every index you add slows down every INSERT and UPDATE, degrading write performance. You are caught in a zero-sum game.

The architectural pattern designed specifically to break this deadlock is Command Query Responsibility Segregation (CQRS).

The Core Philosophy of CQRS

Coined by Greg Young, CQRS proposes a radical departure from traditional CRUD (Create, Read, Update, Delete) architectures.

Instead of a single model that handles both reading and writing, CQRS mandates that you split your system into two entirely separate models:

  1. The Command Model (Writes): Responsible for executing business logic, validating rules, and modifying state.
  2. The Query Model (Reads): Responsible solely for returning data to the client as fast as possible.

These models do not have to share the same code, the same API, or even the same physical database.

Step 1: Segregating at the API Layer

The first step in implementing CQRS is splitting your application logic.

In a CRUD app, you might have a single UserService. In a CQRS app, you have:

  • UserCommandHandler: Handles RegisterUserCommand and ChangeAddressCommand.
  • UserQueryHandler: Handles GetUserProfileQuery and GetActiveUsersQuery.

Commands mutate state and return nothing (or just a success/failure acknowledgment). Queries return data and never mutate state. This strict separation prevents side-effects during read operations and allows you to scale the API tiers independently. You can deploy 50 instances of the Query API for every 1 instance of the Command API.

Step 2: Segregating at the Database Layer

API segregation is useful, but the true power of CQRS is unlocked when you separate the underlying databases.

The Write Database (The Source of Truth)

The Command model interacts with a highly normalized, strictly consistent database (e.g., PostgreSQL). It enforces foreign keys, unique constraints, and ACID transactions. It contains exactly the data needed to enforce business rules, and not a byte more. It is heavily optimized for fast INSERT and UPDATE operations. It has very few indexes.

The Read Database (The Materialized View)

The Query model interacts with a completely different database (e.g., MongoDB, Elasticsearch, or a heavily denormalized Postgres table). This database contains data that is pre-calculated, pre-joined, and formatted exactly as the client needs it.

If the client needs a timeline JSON object containing a post, the author's name, and the top 3 comments, the Read Database stores that exact JSON object as a single document.

When a user requests their timeline, the Query API simply performs a SELECT * FROM timeline_views WHERE user_id = 1. There are no JOINs. There is no business logic. The read operation takes 1 millisecond.

The Sync Problem: Eventual Consistency

If the Command API writes to the Postgres database, and the Query API reads from the MongoDB database, how does the MongoDB database get updated?

This is where CQRS is almost always paired with Event-Driven Architecture.

When the Command API successfully executes a ChangeAddressCommand and updates Postgres, it publishes an event to a message broker (like Kafka or RabbitMQ): AddressChangedEvent { userId: 1, newAddress: '123 Main St' }.

A background worker (the "Projector") listens to this event. When it receives the AddressChangedEvent, it connects to the MongoDB Read Database and updates the pre-calculated user profile document with the new address.

The Cost of Asynchrony

Because the synchronization happens asynchronously via a message broker, the Read Database is Eventually Consistent.

If a user changes their address and immediately refreshes their profile page, the Read Database might not have processed the event yet. The user will see their old address for a few hundred milliseconds.

Your frontend must be designed to handle this (e.g., by optimistically updating the UI in the browser before the network request finishes). If your business requirements demand strict, immediate consistency (like checking a bank balance before an ATM withdrawal), you cannot use an asynchronously synchronized Read Database for that specific query.

CQRS and Event Sourcing

CQRS is frequently combined with Event Sourcing. In this ultimate architecture, the Write Database doesn't even store current state. It only stores the raw, append-only log of events (the Event Store).

  1. Command: DepositMoneyCommand(100)
  2. Write DB (Event Store): Appends MoneyDepositedEvent(100) to disk.
  3. Message Broker: Publishes MoneyDepositedEvent(100).
  4. Projector: Hears the event, calculates currentBalance = currentBalance + 100.
  5. Read DB: Updates the balance_view to 100.

This provides a perfect audit trail on the write side, and perfectly optimized, lightning-fast queries on the read side.

When to Avoid CQRS

Do not implement CQRS for a simple CRUD application. The cognitive load, the operational overhead of maintaining two databases and a message broker, and the user experience hurdles of eventual consistency are massive.

Adopt CQRS only when:

  • Your read/write workloads are vastly asymmetric and require independent scaling.
  • Your read queries are becoming too complex and slow due to normalization constraints.
  • You are building collaborative systems where multiple actors modify the same data concurrently, requiring complex intent-based commands rather than simple state overwrites.

CQRS is a complex architectural pattern, but for high-scale systems, dividing the responsibility between reading and writing is the only way to conquer the database bottleneck.

References

EDITORIAL & AUTHOR NETWORK

Write for InitNode. Earn Proof of Work.

Unlike Medium or Dev.to, InitNode is built exclusively for senior software engineers, infrastructure architects, and systems builders. Every published blueprint is free of paywalls, indexed within seconds, and permanently linked to your verified engineering pedigree.

+250 PoW XP

Climb the Architect Leaderboard and unlock verified reputation badges.

Rich Math & Mermaid

First-class LaTeX math, responsive sequence diagrams, and syntax highlighting.

Instant Indexing

Automated real-time submission to Google Indexing and IndexNow APIs.

Own Your Audience

Readers subscribe directly to you; automated email dispatches on release.

No paywalls. No popups. Strictly high-signal engineering.