Design Patterns

Command Query Responsibility Segregation (CQRS) Design Pattern

I was working on a system where the read path and the write path had completely different performance profiles. Writes were complex — validations, busines...

24 Mar 2024

Command Query Responsibility Segregation (CQRS) Design Pattern

I was working on a system where the read path and the write path had completely different performance profiles. Writes were complex — validations, business rules, event publishing. Reads were simple but needed to be fast. We kept cramming both into the same service, the same models, the same database queries. Everything was a compromise.

CQRS says: stop compromising. Split your system into two sides.

Commands change state. Create a user. Update an order. Cancel a subscription. They run through validation, business rules, and persistence.

Queries read state. Get a user's profile. List recent orders. They're optimized for speed — denormalized views, caching, whatever the read path needs.

The two sides can use different models, different databases, even different languages. They just need to stay in sync.

Javascript
class TaskCommandService {
  constructor() {
    this.tasks = [];
  }

  createTask(task) {
    this.tasks.push(task);
  }

  updateTask(id, updatedTask) {
    const index = this.tasks.findIndex(task => task.id === id);
    if (index !== -1) {
      this.tasks[index] = { ...this.tasks[index], ...updatedTask };
    }
  }
}

class TaskQueryService {
  constructor(taskCommandService) {
    this.taskCommandService = taskCommandService;
  }

  getTasks() {
    return this.taskCommandService.tasks;
  }

  getTaskById(id) {
    return this.taskCommandService.tasks.find(task => task.id === id);
  }
}

const taskCommandService = new TaskCommandService();
const taskQueryService = new TaskQueryService(taskCommandService);

taskCommandService.createTask({ id: 1, title: "Task 1", description: "Description 1" });
taskCommandService.updateTask(1, { title: "Updated Task 1" });

console.log(taskQueryService.getTasks());
console.log(taskQueryService.getTaskById(1));

This is a simplified example — both sides share the same data store. In a real system, the command side writes to one database while projections update a read-optimized store asynchronously.

When CQRS Earns Its Keep

  • Read and write loads differ dramatically. Your app has 100x more reads than writes. Optimize each independently.
  • Read models need different shapes. The write model is normalized. The read model is denormalized for specific UI views.
  • You want event sourcing. CQRS pairs naturally with event sourcing — commands produce events, projections consume them.

Implementing It For Real

  • Define clear boundaries between commands and queries. They should not share models.
  • Design the sync mechanism between write and read stores. This is usually eventual consistency via events.
  • Handle the fact that reads might be stale. Your UI needs to account for "processing" states.
  • Test under concurrent access. Two users updating the same entity while queries are being served from a stale projection — that's where bugs hide.

The benefit: Each side scales independently. Read models are shaped exactly for their consumers. Write models stay clean and focused on business rules. Performance tuning is surgical.

The cost: Two models to maintain instead of one. Eventual consistency means your UI must handle stale data gracefully. Debugging is harder — a bug might be in the command handler, the event publisher, or the projection builder. For simple CRUD apps, CQRS is overkill.

I use CQRS in systems with heavy read loads, complex domain logic on the write side, or where I need event sourcing. For a basic REST API with straightforward reads and writes, a single model is simpler and correct.

Keep reading