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

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.
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
- Event Sourcing Checklist: When It Makes Sense & Common Pitfalls
- Error Boundary React Design Pattern
- Context API React Design Pattern
- Higher-Order Component (HOC) React Design Pattern
- Container Component React Design Pattern
- Best Practices for Structuring Express.js Applications with Prisma Design Pattern