ReviseAlgo Logo

Event-Driven Architecture Patterns

CQRS — Command Query Responsibility Segregation

Separating read databases from write operations using Kafka event synchronization.

Introduction

In simple application architectures, the same database schema and code objects are used to perform both read and write operations. Command Query Responsibility Segregation (CQRS) is an architectural pattern that splits the application into two distinct paths: a **Command** model for mutating operations, and a **Query** model for reporting/reading operations.

Why It Matters

The requirements for reads and writes are often highly asymmetric. Writing requires transaction safety, complex business rule validation, and normalization. Reading requires denormalized views, search indexing, and low latency. Separating them allows scaling and optimization of read databases (e.g. Elasticsearch, Redis) independently from write databases (e.g. PostgreSQL).

Real-World Analogy

Think of CQRS like a newspaper company. The writing department (Command Side) uses word processors, research links, editor comments, and revision histories to draft articles. The publishing press (Query Side) strips out all of those draft details and formats a simple printed newspaper. The final readers query the static paper printed pages; they do not browse the writer's active revision documents.

How It Works

The execution lifecycle of CQRS operates across the following steps:

  1. Command Handling: The client executes a command. The Command Service processes it against the Write DB (e.g. relational DB).
  2. Event Dispatch: The Command Service publishes a CDC or domain event (e.g. ItemUpdated) to Kafka.
  3. Projection Sync: A projection consumer reads the event from Kafka and updates the denormalized Read DB (e.g. search engine).
  4. Query Handling: The client requests a dashboard view. The Query Service queries the Read DB directly, bypassing the Command Service entirely.

Internal Architecture

Because updates flow from the Write DB to the Read DB via Kafka asynchronously, CQRS systems are **Eventually Consistent**. There is a small time window (typically milliseconds) where queries might return slightly outdated information. The system must be designed to tolerate this latency, using techniques like optimistic UI updates or client-side caching.

Visual Explanation

Below is an ASCII representation showing the separate read and write database flows coordinated by Kafka event sync processes:

[User Command] ──> [Command Handler] ──> [Write DB] (RDBMS)
                                               │
                                               ▼ (Emits Event)
[User Query]   <── [Query Handler]   <── [Read DB] (NoSQL) <── [Kafka Sync Consumer]
            

Practical Example

Here is a Java implementation showing a Command side service that handles updates, alongside a separate Projection reader that consumes events to maintain a read-optimized view:

Common Mistakes

  • Applying CQRS to basic CRUD applications: Implementing complex command-query splitting on simple databases that only perform basic inserts/updates. This introduces massive, unnecessary infrastructure overhead.
  • Performing business rules validation on the Query Side: Doing calculation checks in query DTOs. Business validation rules must live strictly inside the Command side.

Quick Quiz

Q1: Why are Write databases in CQRS architectures usually normalized while Read databases are denormalized?

Normalized Write databases prevent data redundancy and guarantee transaction safety. Denormalized Read databases pre-aggregate data structures to ensure sub-millisecond retrieval.

Q2: What is eventual consistency in the context of CQRS?

The temporal lag where the Read database has not yet processed the sync events generated by the Write database, making queries temporarily outdated.

Scenario-Based Challenge

The Challenge: Mitigating user frustration from lag in an order dashboard

A customer clicks "Cancel Order" on your app. The command is successfully processed, but when the UI redirects them to their dashboard, the order status still shows "Active" for about 1 second due to replication lag to the Elasticsearch read database, causing users to submit double cancellations. How do you solve this?

Solution Tip: Implement **optimistic UI updates**. Immediately update the UI state to "Cancelled" in the client browser, or pass the transaction version token in the redirect request and poll/wait until the read replica version catches up.

Debugging Exercise

During a high-throughput flash sale, your Elasticsearch read store becomes heavily out of sync with Postgres. Investigation shows the consumer is processing messages too slowly and lag is mounting.

The Fix: Verify partition allocation. Increase the partition count on your Kafka topics, spawn additional instances of the projection sync consumer, and form a unified consumer group to parallelize processing. Optimize databases via bulk/batch updates instead of making single record write queries.

Interview Questions

1. Can you use CQRS without Event Sourcing?

Yes. CQRS simply segregates read and write paths. While Event Sourcing is frequently paired with CQRS, you can implement CQRS using standard relational databases and synchronize via CRUD logs or CDC.

2. What are the major design risks of implementing CQRS?

Increased system complexity, synchronization lag bugs, dual-write consistency issues, and difficulties in implementing distributed transactions.

Production Considerations

Implement a reliable correlation header flow. When queries require strict, immediate consistency (e.g. validating login passwords), query the Command database directly rather than reading from eventually consistent query caches.