CQRS Pattern
- 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 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
# 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] )
Maintaining the Read Model
# 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
🎯 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.
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.