Command Query Responsibility Segregation (CQRS)
Why reads and writes are different beasts
Command Query Responsibility Segregation (CQRS)
Section titled “Command Query Responsibility Segregation (CQRS)”CQRS sounds fancy, but it’s just recognizing that reading data and writing data are completely different problems. Most systems try to use the same model for both. That’s where things get messy.
Understanding the 80/20 Split
Section titled “Understanding the 80/20 Split”Here’s the thing about most applications: roughly 80% of operations are reads, 20% are writes. But traditional architectures treat them the same.
[Diagram Placeholder: Read vs Write Operation Patterns] Pie chart showing 80% reads, 20% writes
Real-World Examples
Section titled “Real-World Examples”- Writes: Post a status, like a photo, send a message
- Reads: Timeline, notifications, search, friend suggestions
Facebook doesn’t use the same database structure for posting and for showing your timeline. Your timeline is a pre-computed view optimized for fast reads.
Netflix
- Writes: Rate a movie, add to watchlist, update viewing progress
- Reads: Browse catalog, get recommendations, continue watching
Netflix doesn’t query their entire catalog every time you open the app. They have specialized read models for browsing, searching, and recommendations.
Your Bank
- Writes: Transfer money, deposit check, pay bill
- Reads: Check balance, view transactions, generate statements
Banks don’t calculate your balance from scratch every time. They maintain optimized views for different types of queries.
Performance Implications
Section titled “Performance Implications”When you mix reads and writes:
- Writes get slow - Complex joins and indexes for read optimization
- Reads get slow - Normalization and constraints for write consistency
- Scaling is hard - Can’t optimize for both patterns simultaneously
- Conflicts happen - Long-running reports block quick updates
[Diagram Placeholder: Monolithic vs CQRS Performance]
Separating Commands and Queries
Section titled “Separating Commands and Queries”CQRS splits your system into two sides:
Command Side (Writes)
Section titled “Command Side (Writes)”Handles create, update, delete operations:
- Focused on business rules - Validation, consistency, workflows
- Normalized data - Proper relationships, constraints
- Optimized for writes - Fast inserts, updates, deletes
- Authoritative - Source of truth for what happened
// Command side - focused on business logicasync function placeOrder(command: PlaceOrderCommand) { const customer = await customerRepo.findById(command.customerId); const items = await itemRepo.findByIds(command.itemIds);
// Business validation if (!customer.isActive) throw new Error('Customer inactive'); if (items.some(item => !item.inStock)) throw new Error('Item out of stock');
// Create and save order const order = new Order(command.customerId, items); await orderRepo.save(order);
// Publish event await eventBus.publish(new OrderPlaced(order.id, order.total));}
Query Side (Reads)
Section titled “Query Side (Reads)”Handles all read operations and projections:
- Focused on user experience - Fast queries, rich data
- Denormalized data - Pre-computed views, redundant storage
- Optimized for reads - Indexes, caching, search
- Eventually consistent - Updated from command side events
// Query side - optimized for fast readsasync function getOrderHistory(customerId: string) { // Pre-computed view with all needed data return await orderHistoryView.findByCustomerId(customerId);}
async function getOrderSummary(orderId: string) { // Specialized view for order details return await orderSummaryView.findById(orderId);}
[Diagram Placeholder: Command and Query Side Architecture]
Independent Scaling and Optimization
Section titled “Independent Scaling and Optimization”With CQRS, you can:
Scale reads and writes independently
- Command side: Fewer, more powerful servers
- Query side: Many read replicas, CDN caching
Use different databases
- Command side: PostgreSQL for ACID transactions
- Query side: Elasticsearch for search, Redis for caching
Optimize for different patterns
- Command side: Normalized, consistent, durable
- Query side: Denormalized, fast, eventually consistent
Database Flexibility Per Use Case
Section titled “Database Flexibility Per Use Case”Command Side Options
Section titled “Command Side Options”- PostgreSQL - ACID transactions, complex business rules
- Event stores - Append-only, audit trails, event sourcing
- MongoDB - Document-based aggregates, flexible schema
Query Side Options
Section titled “Query Side Options”- Elasticsearch - Full-text search, analytics, aggregations
- Redis - Caching, real-time data, pub/sub
- ClickHouse - Analytics, time-series, reporting
- Read replicas - Scaled-out traditional databases
[Diagram Placeholder: Multi-Database CQRS Architecture]
Building Projections
Section titled “Building Projections”Projections are how you build your read models from command side events:
// Order summary projectionclass OrderSummaryProjection { async handle(event: OrderEvent) { switch (event.type) { case 'OrderPlaced': await this.createOrderSummary(event); break; case 'OrderShipped': await this.updateShippingStatus(event); break; case 'OrderDelivered': await this.markAsDelivered(event); break; } }
private async createOrderSummary(event: OrderPlaced) { const summary = { orderId: event.orderId, customerId: event.customerId, total: event.total, status: 'placed', items: event.items.map(item => ({ name: item.name, price: item.price, quantity: item.quantity })) };
await this.summaryStore.save(summary); }}
When to Use CQRS
Section titled “When to Use CQRS”Good Candidates
Section titled “Good Candidates”- Complex business logic - Order processing, financial transactions
- High read/write ratio - Social media, content platforms
- Multiple read patterns - Admin dashboards, user views, reports
- Performance requirements - Need to scale reads independently
Skip CQRS When
Section titled “Skip CQRS When”- Simple CRUD - Basic user management, settings
- Low traffic - Internal tools, prototypes
- Team complexity - Small teams, tight deadlines
- Data consistency critical - Real-time trading, inventory
Common Patterns
Section titled “Common Patterns”Synchronous Projections
Section titled “Synchronous Projections”Update read models immediately when commands execute:
await commandHandler.handle(command);await projectionHandler.update(resultingEvents);
Asynchronous Projections
Section titled “Asynchronous Projections”Update read models via event handlers:
eventBus.subscribe('OrderPlaced', orderSummaryProjection.handle);eventBus.subscribe('OrderShipped', orderSummaryProjection.handle);
Snapshot Projections
Section titled “Snapshot Projections”Rebuild read models from scratch periodically:
async function rebuildOrderSummaries() { const allOrderEvents = await eventStore.readAll(); await orderSummaryProjection.rebuild(allOrderEvents);}
Getting Started
Section titled “Getting Started”- Identify read/write patterns - Where do you have complex queries?
- Start with one projection - Don’t split everything at once
- Keep commands simple - Focus on business rules, not read optimization
- Build projections incrementally - Add views as you need them
CQRS isn’t about being clever. It’s about recognizing that reads and writes have different needs, and giving each what they need to succeed.