Skip to content
Home System Design CQRS Pattern

CQRS Pattern

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Architecture → Topic 5 of 13
CQRS (Command Query Responsibility Segregation) explained — separating reads from writes, read models, event sourcing connection, when to use CQRS, and implementation example.
🔥 Advanced — solid System Design foundation required
In this tutorial, you'll learn
CQRS (Command Query Responsibility Segregation) explained — separating reads from writes, read models, event sourcing connection, when to use CQRS, and implementation example.
  • CQRS separates write model (commands) from read model (queries) — optimise each independently.
  • Read models are denormalised and pre-computed — fast reads, no joins at query time.
  • Read models are eventually consistent — updated asynchronously via events.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer

CQRS separates the write model (commands that change state) from the read model (queries that return data). Commands go to the write side (normalised database, business logic). Queries go to the read side (denormalised, optimised for display). They can use different databases, optimised for their specific access patterns.

The Core Pattern

Example · PYTHON
1234567891011121314151617181920212223242526272829303132333435363738394041
# Package: io.thecodeforge.python.system_design

# COMMAND: changes state — returns nothing or just an ID
class CreateOrderCommand:
    def __init__(self, user_id: int, items: list, total: float):
        self.user_id = user_id
        self.items = items
        self.total = total

class OrderCommandHandler:
    def handle(self, cmd: CreateOrderCommand) -> int:
        # Business logic: validate, apply rules
        if cmd.total <= 0:
            raise ValueError('Order total must be positive')

        # Write to normalised store
        order_id = orders_db.insert({
            'user_id': cmd.user_id,
            'total': cmd.total,
            'status': 'pending'
        })
        for item in cmd.items:
            order_items_db.insert({'order_id': order_id, **item})

        # Publish event for read model update
        event_bus.publish('OrderCreated', {'order_id': order_id, **vars(cmd)})
        return order_id

# QUERY: reads state — returns data, changes nothing
class GetUserOrdersQuery:
    def __init__(self, user_id: int, page: int = 1):
        self.user_id = user_id
        self.page = page

class OrderQueryHandler:
    def handle(self, query: GetUserOrdersQuery):
        # Read from DENORMALISED read model — no joins needed
        return read_db.query(
            'SELECT * FROM user_orders_view WHERE user_id = ? ORDER BY created_at DESC LIMIT 20 OFFSET ?',
            [query.user_id, (query.page - 1) * 20]
        )
▶ Output
# Commands write to normalised DB; queries read from denormalised view

Maintaining the Read Model

Example · PYTHON
1234567891011121314151617181920212223
# Read model is updated asynchronously by consuming events
class OrderProjection:
    """Keeps user_orders_view up to date by handling OrderCreated events."""

    def on_order_created(self, event):
        # Denormalise: join order + user + items into one read-optimised row
        user = users_db.get(event['user_id'])
        items = event['items']

        read_db.upsert('user_orders_view', {
            'order_id':   event['order_id'],
            'user_id':    event['user_id'],
            'user_name':  user['name'],         # denormalised
            'user_email': user['email'],         # denormalised
            'total':      event['total'],
            'item_count': len(items),            # pre-computed
            'item_names': ', '.join(i['name'] for i in items),  # pre-joined
            'status':     'pending',
            'created_at': event['timestamp']
        })

# Trade-off: read model is eventually consistent with the write model
# Between OrderCreated event and projection update, a brief window of inconsistency
▶ Output
# Read model updated asynchronously — eventual consistency

🎯 Key Takeaways

  • CQRS separates write model (commands) from read model (queries) — optimise each independently.
  • Read models are denormalised and pre-computed — fast reads, no joins at query time.
  • Read models are eventually consistent — updated asynchronously via events.
  • CQRS complexity is high — use only when read/write performance requirements genuinely diverge.
  • CQRS pairs naturally with Event Sourcing but does not require it.

Interview Questions on This Topic

  • QWhat is CQRS and what problem does it solve?
  • QWhat is eventual consistency in the context of CQRS?
  • QWhat is the difference between CQRS and Event Sourcing?

Frequently Asked Questions

What is the difference between CQRS and Event Sourcing?

CQRS separates reads from writes. Event Sourcing stores every state change as an immutable event — you derive current state by replaying events. They are separate patterns that work well together: Event Sourcing for the write side (events are the write model), CQRS for the read side (projections build read models from events). You can use CQRS without Event Sourcing.

When should I NOT use CQRS?

For most CRUD applications — the added complexity (two models, event propagation, eventual consistency) is not justified when reads and writes have similar requirements. Start simple. Add CQRS when you have measurable read/write performance problems, when the read and write models diverge significantly, or when you need audit trails that Event Sourcing provides.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousEvent-Driven ArchitectureNext →Event Sourcing
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged