Introduction to CQRS
CQRS stands for Command and Query Responsibility Segregation is a design pattern and that separates the responsibilities of read and write operations in an application. By decoupling command and query processing, CQRS promotes scalability, maintainability, and flexibility in software architectures.
Queries Perform read operation and Command perform writes operation like create, update, delete and return data.
As we know, in our application we mostly use single data model to read and write data, which will work fine and perform CRUD operation easily but when application is vast in that case our queries return different types of data as an object so that it become hard to manage with different DTO objects.
Also, the same model is used to perform a write operation. As result, the model becomes more complex, Also, when we use same model for both read and write operation the security also hard to manage when application is large, and entity might expose data in wrong context due to the workload on the same model.
Core concepts of the CQRS pattern:
- Commands: Commands represent actions that mutate the state of the system. They are typically imperative and result in state changes.
- Queries: Queries represent read operations that retrieve data from the system. They are typically declarative and do not modify the state of the system.
- Command Handlers: Command handlers are responsible for executing commands and updating the system’s state accordingly.
- Query Handlers: Query handlers are responsible for executing queries and retrieving data from the system.
When to use CQRS pattern
- When application is huge and access the same data in parallel.
- CQRS helps reduce the merge conflict while performing multiple operation with data.
- In DDD terminology, if domain data model is complex and need to perform many operations on priority like validation and executing some business logic so in that case, we need consistency.
- When has a high demand for data consumption.
- Performance of data read must be tuned separately from the performance of data writes, especially when the number of read is much greater than the number of writes.
- There is a need for a team to focus on the complex domain model that is part of write model, while another team can focus on the read model and user interface.
Implementation Steps
We will walk through the implementation of CQRS in a .NET application using a simple example. We’ll create separate components for commands, command handlers, queries, and query handlers.
Folder Structure
Let’s first look at the two repositories (These are in the repositories folder)
The Commands (Write) repository
using ConsoleAppCQRSPattern.Models;
namespace ConsoleAppCQRSPattern.Repositories {
public interface IEmployeeCommandsRepository {
void SaveEmployee(Employee employee);
}
}
using ConsoleApp CQRSPattern.Models;
namespace ConsoleApp CQRSPattern.Repositories {
public class EmployeeCommandsRepository: IEmployeeCommandsRepository {
public void SaveEmployee(Employee employee) {
// Persist the employee record in a data store
}
}
}
Here we see that only write operations (in this case only one) are included.
The Queries (Read) repository
using ConsoleAppCQRSPattern.Models;
namespace ConsoleAppCQRSPattern.Repositories {
public interface IEmployeeQueriesRepository {
Employee GetByID(int employeeID);
}
}
using System;
using ConsoleAppCQRSPattern.Models;
namespace ConsoleAppCQRSPattern.Repositories {
public class EmployeeQueriesRepository: IEmployeeQueriesRepository {
public Employee GetByID(int employeeID) {
// Get the employee record from a data store
// Below is for demo purposes only
return new Employee() {
Id = 100,
FirstName = "John",
LastName = "Smith",
DateOfBirth = new DateTime(2000, 1, 1),
Street = "100 Toronto Street",
City = "Toronto",
PostalCode = "k1k 1k1"
};
}
}
}
Here we see that only read operations (in this case only one) are included.
Next, we look at the two middle-tier components (These are in the Queries and Commands folders),
Queries (To read data)
using ConsoleAppCQRSPattern.DTOs;
namespace ConsoleAppCQRSPattern.Queries {
public interface IEmployeeQueries {
EmployeeDTO FindByID(int employeeID);
}
}
using System;
using ConsoleAppCQRSPattern.Repositories;
using ConsoleAppCQRSPattern.DTOs;
namespace ConsoleAppCQRSPattern.Queries {
public class EmployeeQueries {
private readonly IEmployeeQueriesRepository _repository;
public EmployeeQueries(IEmployeeQueriesRepository repository) {
_repository = repository;
}
public EmployeeDTO FindByID(int employeeID) {
var emp = _repository.GetByID(employeeID);
return new EmployeeDTO {
Id = emp.Id,
FullName = emp.FirstName + " " + emp.LastName,
Address = emp.Street + " " + emp.City + " " + emp.PostalCode,
Age = Convert.ToInt32(Math.Abs(((DateTime.Now - emp.DateOfBirth).TotalDays) / 365)) - 1
};
}
}
}
Commands (To write data)
using ConsoleAppCQRSPattern.Models;
namespace ConsoleAppCQRSPattern.Commands {
public interface IEmployeeCommands {
void SaveEmployeeData(Employee employee);
}
}
using ConsoleAppCQRSPattern.Models;
using ConsoleAppCQRSPattern.Repositories;
namespace ConsoleAppCQRSPattern.Commands {
public class EmployeeCommands: IEmployeeCommands {
private readonly IEmployeeCommandsRepository _repository;
public EmployeeCommands(IEmployeeCommandsRepository repository) {
_repository = repository;
}
public void SaveEmployeeData(Employee employee) {
_repository.SaveEmployee(employee);
}
}
}
Here we see that we have separate operation handling components. Two other classes are the main Employee class which mirrors our storage item and the Employee DTO class which is used by the Queries (Read) operations to return data in a shape required by the consuming client.
namespace ConsoleAppCQRSPattern.DTOs {
public class EmployeeDTO {
public int Id {
get;
set;
}
public string FullName {
get;
set;
}
public int Age {
get;
set;
}
public string Address {
get;
set;
}
}
}
using System;
namespace ConsoleAppCQRSPattern.Models {
public class Employee {
public int Id {
get;
set;
}
public string FirstName {
get;
set;
}
public string LastName {
get;
set;
}
public DateTime DateOfBirth {
get;
set;
}
public string Street {
get;
set;
}
public string City {
get;
set;
}
public string PostalCode {
get;
set;
}
}
}
Finally, we call these operations from the main Program class. Please note that in this demo application, we create an instance of the repository directly. In a real-world scenario, we would probably use some dependency injection framework to do this.
using ConsoleAppCQRSPattern.Commands;
using ConsoleAppCQRSPattern.Queries;
using ConsoleAppCQRSPattern.Repositories;
using System;
namespace ConsoleAppCQRSPattern {
classProgram {
staticvoid Main(string[] args) {
// Command the Employee Domain to save data
var employeeCommand = new EmployeeCommands(new EmployeeCommandsRepository());
employeeCommand.SaveEmployeeData(new Models.Employee {
Id = 200, FirstName = "Jane", LastName = "Smith", Street = "150 Toronto Street", City = "Toronto", PostalCode = "j1j1j1", DateOfBirth = new DateTime(2002, 2, 2)
});
Console.WriteLine($ "Employee data stored");
// Query the Employee Domain to get data
var employeeQuery = new EmployeeQueries(new EmployeeQueriesRepository());
var employee = employeeQuery.FindByID(100);
Console.WriteLine($ "Employee ID:{employee.Id}, Name:{employee.FullName}, Address:{employee.Address}, Age:{employee.Age}");
Console.ReadKey();
}
}
}
When we run this application, we see the below output,
Advantages of CQRS
- Separation of concern – when read and write side are separated, the model become more maintainable and flexible. The read model is typically simpler, while write model involves more complex business logic.
- Optimized data schemas – On read side, a schema can be optimized for queries and on write side, a schema can be optimized for updates.
- Simpler queries – Application queries can avoid complex join by storing a materialized view in the read database.
- Security – it’s easier to ensure that only the right domain entities are performing writes on the data.
Conclusion:
In this blog post, we explored how to implement the Command Query Responsibility Segregation (CQRS) pattern in .NET applications. By following the steps outlined above, developers can create scalable, maintainable, and flexible software architectures that separate command and query processing effectively. Add their benefits in large scale applications.


