0
0
Microservicessystem_design~7 mins

CQRS pattern in Microservices - System Design Guide

Choose your learning style9 modes available
Problem Statement
When a system uses the same model for both reading and writing data, it often faces performance bottlenecks and complexity. Heavy read and write operations interfere with each other, causing slow responses and difficult scaling. Additionally, complex business logic for writes can make queries inefficient and hard to optimize.
Solution
CQRS separates the system into two parts: one handles commands (writes) and the other handles queries (reads). Each part uses its own data model optimized for its task. This separation allows independent scaling, simpler queries, and better performance by isolating read and write workloads.
Architecture
Client
Command Model
Read Store
Query Client

This diagram shows how client requests split into commands and queries. Commands update the write store and publish events. Events update the read store, which serves queries separately.

Trade-offs
✓ Pros
Improves performance by optimizing read and write models separately.
Allows independent scaling of read and write workloads.
Simplifies complex queries by using a dedicated read model.
Enables eventual consistency, improving system responsiveness.
✗ Cons
Increases system complexity due to separate models and synchronization.
Requires handling eventual consistency, which may confuse users.
Adds overhead of maintaining event or message infrastructure.
Use CQRS when the system has high read/write load imbalance, complex queries, or requires independent scaling of reads and writes, typically at scale above thousands of requests per second.
Avoid CQRS for simple CRUD applications with low traffic (under 1000 requests per second) or when strong immediate consistency is mandatory.
Real World Examples
Amazon
Amazon uses CQRS to separate order processing commands from product catalog queries, enabling efficient scaling and complex query handling.
Uber
Uber applies CQRS to handle ride requests (commands) separately from real-time ride status queries, improving responsiveness and scalability.
LinkedIn
LinkedIn uses CQRS to manage user profile updates and feed queries independently, optimizing for heavy read traffic.
Code Example
This code shows how CQRS separates write and read responsibilities into different classes. The command model updates data and publishes events. The query model reads from a separate store updated by event handlers. This separation improves scalability and query performance.
Microservices
### Before CQRS (single model for read/write)
class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

    def update_email(self, new_email):
        self.email = new_email

    def get_user_info(self):
        return {'name': self.name, 'email': self.email}


### After CQRS (separate command and query models)

# Command model handles writes
class UserCommandModel:
    def __init__(self, user_store):
        self.user_store = user_store

    def update_email(self, user_id, new_email):
        user = self.user_store.get(user_id) or {}
        user['email'] = new_email
        self.user_store[user_id] = user
        # Publish event for read model update
        EventBus.publish('UserEmailUpdated', {'user_id': user_id, 'email': new_email})

# Query model handles reads
class UserQueryModel:
    def __init__(self, read_store):
        self.read_store = read_store

    def get_user_info(self, user_id):
        return self.read_store.get(user_id)

# Event handler updates read store
class EventBus:
    subscribers = {}

    @classmethod
    def publish(cls, event_type, data):
        for handler in cls.subscribers.get(event_type, []):
            handler(data)

    @classmethod
    def subscribe(cls, event_type, handler):
        cls.subscribers.setdefault(event_type, []).append(handler)

# Mock read store for demonstration
read_store = {}

# Example event handler
def handle_user_email_updated(event):
    user_id = event['user_id']
    email = event['email']
    read_user = read_store.get(user_id) or {}
    read_user['email'] = email
    read_store[user_id] = read_user

EventBus.subscribe('UserEmailUpdated', handle_user_email_updated)

# Explanation:
# The before code mixes reading and writing in one model.
# The after code separates command (write) and query (read) models.
# Commands update the write store and publish events.
# Events update the read store asynchronously, enabling optimized queries.
OutputSuccess
Alternatives
CRUD
Uses a single model for both reads and writes without separation.
Use when: Choose CRUD for simple applications with balanced read/write loads and minimal complexity.
Event Sourcing
Stores all changes as events and rebuilds state from them; CQRS can be combined with event sourcing but event sourcing focuses on state persistence.
Use when: Choose event sourcing when auditability and full history of changes are required.
Summary
CQRS separates read and write operations into different models to improve performance and scalability.
It allows independent optimization and scaling of queries and commands.
CQRS introduces complexity and eventual consistency, so it suits systems with complex workloads and high scale.