Skip to content

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.

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

Facebook

  • 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.

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]

CQRS splits your system into two sides:

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 logic
async 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));
}

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 reads
async 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]

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
  • PostgreSQL - ACID transactions, complex business rules
  • Event stores - Append-only, audit trails, event sourcing
  • MongoDB - Document-based aggregates, flexible schema
  • 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]

Projections are how you build your read models from command side events:

// Order summary projection
class 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);
}
}
  • 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
  • Simple CRUD - Basic user management, settings
  • Low traffic - Internal tools, prototypes
  • Team complexity - Small teams, tight deadlines
  • Data consistency critical - Real-time trading, inventory

Update read models immediately when commands execute:

await commandHandler.handle(command);
await projectionHandler.update(resultingEvents);

Update read models via event handlers:

eventBus.subscribe('OrderPlaced', orderSummaryProjection.handle);
eventBus.subscribe('OrderShipped', orderSummaryProjection.handle);

Rebuild read models from scratch periodically:

async function rebuildOrderSummaries() {
const allOrderEvents = await eventStore.readAll();
await orderSummaryProjection.rebuild(allOrderEvents);
}
  1. Identify read/write patterns - Where do you have complex queries?
  2. Start with one projection - Don’t split everything at once
  3. Keep commands simple - Focus on business rules, not read optimization
  4. 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.