ASP.NET Core  

CQRS in ASP.NET Core: Practical Scenarios Where It Actually Makes Sense

Introduction

CQRS (Command Query Responsibility Segregation) is one of the most discussed architectural patterns in modern .NET development. It frequently appears in conversations about microservices, Domain-Driven Design (DDD), Event Sourcing, and scalable enterprise systems. However, many developers encounter CQRS through tutorials that make it seem like a solution every application should adopt.

The reality is quite different.

CQRS can provide significant benefits in the right scenarios, but it also introduces additional complexity. Applying it to simple CRUD applications often leads to unnecessary abstractions, extra code, and increased maintenance costs. The key is understanding when CQRS solves real business problems and when traditional approaches are sufficient.

In this article, we'll explore what CQRS is, how it works in ASP.NET Core, and most importantly, the practical situations where it actually makes sense to use it.

What Is CQRS?

CQRS stands for Command Query Responsibility Segregation.

The pattern separates operations into two categories:

Commands

Commands modify data.

Examples:

  • Create Order

  • Update Customer

  • Delete Product

  • Approve Invoice

Commands change the system state.

Queries

Queries retrieve data.

Examples:

  • Get Order Details

  • List Products

  • Search Customers

  • View Reports

Queries should not modify data.

Instead of a single model handling both operations:

Application
      ↓
Single Data Model
      ↓
Read + Write Operations

CQRS separates them:

Commands
      ↓
Write Model

Queries
      ↓
Read Model

This separation allows each side to evolve independently.

Traditional CRUD vs CQRS

Consider a standard product management application.

Traditional approach:

public class ProductService
{
    public Product GetProduct(int id) { }

    public void CreateProduct(Product product) { }

    public void UpdateProduct(Product product) { }

    public void DeleteProduct(int id) { }
}

Everything is handled by a single service.

With CQRS:

CreateProductCommand
UpdateProductCommand
DeleteProductCommand

GetProductQuery
GetProductsQuery

Commands and queries are handled separately.

This structure improves flexibility but also increases complexity.

Why CQRS Exists

The main goal of CQRS is to address situations where read and write requirements differ significantly.

In many systems:

  • Reads greatly outnumber writes

  • Read models differ from write models

  • Different scalability requirements exist

  • Business logic is complex

For example:

10 Writes Per Second
1000 Reads Per Second

Using the same model for both operations may not be optimal.

CQRS enables independent optimization of each side.

Scenario 1: Complex Business Workflows

CQRS shines when business operations contain substantial logic.

Consider an order management system.

Creating an order may involve:

  • Inventory validation

  • Discount calculations

  • Payment verification

  • Tax computation

  • Event publishing

Example command:

public record CreateOrderCommand(
    int CustomerId,
    List<OrderItem> Items);

The command handler can encapsulate all business rules.

public class CreateOrderHandler
{
    public async Task Handle(
        CreateOrderCommand command)
    {
        // Business logic
    }
}

Meanwhile, queries remain focused on retrieving information.

This separation keeps business logic organized and maintainable.

Scenario 2: Read-Heavy Applications

Many applications perform significantly more reads than writes.

Examples include:

  • E-commerce platforms

  • News websites

  • Learning portals

  • Product catalogs

A typical workload may look like:

Product Updates
      ↓
100 Per Day

Product Views
      ↓
500,000 Per Day

In these situations, read models can be optimized independently.

For example:

Write Database
      ↓
Read Database

Queries can use denormalized structures designed specifically for fast retrieval.

This improves performance without affecting write operations.

Scenario 3: Dashboard and Reporting Systems

Reporting requirements often differ dramatically from transactional requirements.

Consider a sales dashboard.

Users may request:

  • Revenue by region

  • Monthly trends

  • Top-selling products

  • Customer analytics

These queries typically require:

  • Aggregations

  • Joins

  • Complex calculations

Instead of executing expensive queries against transactional tables, CQRS enables dedicated read models optimized for reporting.

Example:

Order Database
      ↓
Projection
      ↓
Reporting Database

This improves reporting performance while reducing load on operational systems.

Scenario 4: Microservices Architectures

CQRS often fits naturally within microservices environments.

Consider:

Order Service
      ↓
Inventory Service
      ↓
Payment Service

Each service may expose:

  • Commands for state changes

  • Queries for data retrieval

The separation helps teams maintain clear boundaries between business operations and data access.

CQRS also works well with event-driven architectures where commands trigger domain events.

Scenario 5: Event Sourcing Systems

Event Sourcing and CQRS are frequently used together.

Architecture:

Command
      ↓
Aggregate
      ↓
Event Store
      ↓
Read Projection
      ↓
Query

Commands generate events.

Queries access read models built from those events.

Although CQRS does not require Event Sourcing, the two patterns complement each other effectively.

When CQRS Does NOT Make Sense

One of the biggest misconceptions is that CQRS improves every application.

Consider a simple employee management system:

Create Employee
Update Employee
Delete Employee
Get Employee

Business logic is straightforward.

Read and write requirements are similar.

A traditional service layer is usually sufficient.

Implementing CQRS may introduce:

  • Additional handlers

  • More files

  • More abstractions

  • Increased complexity

without providing meaningful benefits.

If the application is primarily CRUD-based, CQRS may be unnecessary.

Implementing CQRS with MediatR

Many ASP.NET Core applications implement CQRS using MediatR.

Query:

public record GetProductQuery(
    int ProductId)
    : IRequest<Product>;

Handler:

public class GetProductHandler
    : IRequestHandler<
        GetProductQuery,
        Product>
{
    public async Task<Product> Handle(
        GetProductQuery request,
        CancellationToken token)
    {
        // Retrieve product
    }
}

Controller:

[HttpGet("{id}")]
public async Task<Product>
Get(int id)
{
    return await _mediator.Send(
        new GetProductQuery(id));
}

This approach keeps controllers thin and business logic centralized.

However, avoid creating handlers for trivial operations that provide little value.

Benefits of CQRS

Better Separation of Concerns

Commands and queries have distinct responsibilities.

Improved Scalability

Read and write workloads can scale independently.

Cleaner Business Logic

Complex operations become easier to organize.

Optimized Read Models

Read-side performance can be improved without impacting writes.

Better Support for Event-Driven Systems

CQRS integrates naturally with domain events and Event Sourcing.

Challenges of CQRS

Increased Complexity

Additional layers require more code and maintenance.

More Files and Components

Even simple features may require multiple classes.

Learning Curve

Developers must understand commands, queries, handlers, and messaging patterns.

Potential Overengineering

Applying CQRS to simple applications often creates unnecessary complexity.

These trade-offs should be carefully evaluated before adoption.

Best Practices

Start Simple

Begin with traditional architecture unless there is a clear need for CQRS.

Apply CQRS Selectively

Not every feature requires command-query separation.

Some applications benefit from using CQRS only in specific modules.

Focus on Business Complexity

The more complex the domain, the more valuable CQRS becomes.

Avoid Handler Explosion

Do not create unnecessary commands and queries for trivial operations.

Combine with Clean Architecture

CQRS often works well within Clean Architecture implementations because responsibilities are clearly separated.

Conclusion

CQRS is a powerful architectural pattern, but it is not a universal solution. Its greatest value appears in applications with complex business workflows, read-heavy workloads, reporting requirements, microservices architectures, and event-driven systems. In these scenarios, separating commands from queries can improve scalability, maintainability, and overall system design.

However, for simple CRUD applications, CQRS often introduces complexity without delivering significant benefits. The most effective approach is to evaluate business requirements first and adopt CQRS only when it solves a real problem.

For ASP.NET Core developers, understanding when to use CQRS is far more important than knowing how to implement it. Applied thoughtfully, CQRS can become a valuable tool for building scalable and maintainable enterprise applications. Applied indiscriminately, it can quickly become unnecessary architectural overhead.