Sunday, November 9, 2025

Architecture - CQRS (Command Query Responsibility Segregation)

 ðŸ§© 1. What is CQRS?

CQRS stands for:

Command
Query
Responsibility
Segregation

It means:

Separate the read (Query) and write (Command) operations of your application into different models.

 


Problem with traditional architectures and how CQRS pattern solves it-

Traditional architectures often face challenges in handling high loads and managing complex data requirements. In these systems, the same model is used for both reading (fetching data) and writing (updating data), which can lead to performance issues. As the application grows, handling large read and write requests together becomes harder, creating bottlenecks and slowing down responses.

CQRS solves this by separating read and write operations into distinct models.

  • This means write requests (commands) and read requests (queries) are processed independently, optimizing each for its specific task.
  • As a result, CQRS allows systems to handle higher traffic efficiently, improves performance, and simplifies scaling by allowing independent optimization of read and write parts.


How to Sync Databases with CQRS Design Pattern -
Synchronizing databases in a system that follows the CQRS pattern can be challenging due to the separation of the write and read sides of the application.

How-to-Sync-Databases-with-CQRS


Here is how you can handle the synchronization:

  1. Write to the Command Database: When you make changes (create, update, delete), they are first saved in the command database. This database is optimized for handling write operations.
  2. Generate Events: After the write operation is successful, the system generates events that describe what changed (like "Order Created" or "User Updated"). These events serve as notifications about the updates.
  3. Update the Query Database: The read database, optimized for fast queries, listens for these events and applies the changes to its own copy of the data. This way, the query database gets updated with the latest information.
  4. Eventual Consistency: The key idea is that the query database doesn’t have to update immediately. There can be a slight delay, but eventually, both databases will sync, ensuring consistency over time.


Challenges of using CQRS Design Pattern

Below are the challenges of using CQRS Design Pattern:
  • Complexity: Your system may become more complex if you use CQRS, particularly if you are unfamiliar with the pattern. It can be difficult to coordinate data synchronization, manage distinct read and write models, and guarantee consistency between the two.
  • Consistency: Maintaining consistency between the read and write models can be challenging, especially in distributed systems where data updates may not propagate immediately. Careful planning and execution are necessary to guarantee stability over time without compromising scalability or performance.
  • Data Synchronization: It might be difficult to keep the read and write models in sync, particularly when handling complicated data transformations or big data sets. It can be beneficial to use strategies like message queues or event sources.
  • Performance Overhead: Implementing CQRS can introduce performance overhead, especially if not done carefully. For example, using event sourcing for the write model can impact write performance, while keeping the read model updated in real-time can impact read performance.
  • Operational Complexity: Operational complexity may rise while managing two databases or data storage (one for read and one for write). This covers duties including monitoring, backup and restoration, and guaranteeing data durability and high availability.

Best Practices for implementing CQRS pattern

Below are some of the best practices for implementing CQRS pattern:

Separate Read and Write Models Carefully:
Clearly divide the system into models for reading data (queries) and writing data (commands). This separation helps keep each model simple and optimized for its specific task.

Use Asynchronous Communication When Needed:
Since commands and queries are separated, consider using asynchronous messaging for commands. This helps the system stay responsive and handle high traffic efficiently, even if some operations take longer.

Keep Commands and Queries as Simple as Possible:
Design commands to focus only on changing data (like “CreateOrder” or “UpdateUser”) and queries only on retrieving data (like “GetOrderDetails”). Avoid mixing read and write logic in either part to keep things clean and maintainable.

Embrace Event Sourcing for Data Consistency:
Event sourcing can be paired with CQRS to keep a record of all changes. Each change is saved as an event, and the current state is rebuilt from these events. This can make it easier to track history, recover data, or audit changes.

Consider the Complexity of Your System:
CQRS adds some complexity, so it’s best suited for systems with high read and write demands or complex business rules. For simpler systems, CQRS might be overkill and add unnecessary development overhead.


⚙️ Without CQRS (Traditional CRUD)

In a typical system, we use a single model (e.g., Product) for everything:

public class ProductController : Controller { private readonly AppDbContext _context; public ProductController(AppDbContext context) { _context = context; } public IActionResult Get(int id) => Ok(_context.Products.Find(id)); public IActionResult Create(Product product)     {
        _context.Add(product); _context.SaveChanges(); return Ok();
    } }

🔸 Both read and write share the same entity (Product).
🔸 Works fine for small systems, but becomes hard to optimize and scale.


⚙️ With CQRS

We split the system into two logical parts:

TypeResponsibilityTypical OperationExample
CommandChanges system state (writes)Create / Update / DeleteCreateOrder, UpdateUser
QueryReads data onlyRead / GetGetOrders, GetUserById

➡️ Each side can have its own model, own data access, even separate databases.


🧠 2. Why CQRS?

BenefitDescription
ScalabilityYou can scale reads and writes independently.
PerformanceReads can use denormalized or cached data for speed.
Simplicity per sideEach side (Command/Query) is simpler and more focused.
MaintainabilityEasier to evolve business logic (especially in DDD).
Event-driven integrationWorks great with Event Sourcing and Message Queues.

🧩 3. CQRS in .NET Example

Using MediatR (popular CQRS library for .NET):

Command:

public record CreateOrderCommand(string ProductName, int Quantity) : IRequest<int>;

Command Handler:

public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, int> { private readonly AppDbContext _context; public CreateOrderHandler(AppDbContext context) => _context = context; public async Task<int> Handle(CreateOrderCommand request, CancellationToken cancellationToken) { var order = new Order { ProductName = request.ProductName, Quantity = request.Quantity }; _context.Orders.Add(order); await _context.SaveChangesAsync(); return order.Id; } }

Query:

public record GetOrderByIdQuery(int Id) : IRequest<Order>;

Query Handler:

public class GetOrderByIdHandler : IRequestHandler<GetOrderByIdQuery, Order> { private readonly AppDbContext _context; public GetOrderByIdHandler(AppDbContext context) => _context = context; public async Task<Order> Handle(GetOrderByIdQuery request, CancellationToken cancellationToken) { return await _context.Orders.FindAsync(request.Id); } }

Controller:

public class OrderController : ControllerBase { private readonly IMediator _mediator; public OrderController(IMediator mediator) => _mediator = mediator; [HttpPost] public async Task<IActionResult> Create(CreateOrderCommand command) => Ok(await _mediator.Send(command)); [HttpGet("{id}")] public async Task<IActionResult> Get(int id) => Ok(await _mediator.Send(new GetOrderByIdQuery(id))); }

🧭 4. CQRS + Event Sourcing (Advanced)

Often used together:

  • Command side emits Events (e.g., OrderCreatedEvent)
  • Query side listens to those events and updates read models (denormalized views or cache).

So data flows asynchronously, improving performance and scalability.


🧠 5. When to Use (and When Not)

Use CQRS WhenAvoid CQRS When
Complex domain logicSimple CRUD apps
High read/write loadSmall internal tools
Need for scalabilityShort-lived prototypes
Event-driven architectureTraditional monolith

📊 6. Summary

AspectCommand SideQuery Side
PurposeModify dataRead data
ModelRich domain modelDTOs / View Models
DBOLTP (normalized)Read DB / Cache
ExampleCreateOrderCommandGetOrdersQuery


No comments:

Post a Comment

CI/CD - Safe DB Changes/Migrations

Safe DB Migrations means updating your database schema without breaking the running application and without downtime . In real systems (A...