Skip to main content

Introduction

CQRS (Command Query Responsibility Segregation) is a pattern that separates read and write operations into distinct models. The Hybrid DDD Architecture implements CQRS using MediatR to provide clear separation between commands (writes) and queries (reads).

CQRS Principles

Core Concept

CQRS is based on the principle that the methods that change system state (commands) should be separated from methods that read system state (queries).
Command: Writes → Change state → Returns success/ID
Query:   Reads  → No side effects → Returns data

Benefits

Scalability

Read and write workloads can be scaled independently

Optimization

Queries can be optimized differently from commands

Clarity

Clear distinction between operations that change state vs read state

Flexibility

Different models for reading and writing (e.g., denormalized read models)

Architecture Overview

The CQRS implementation in this template uses the following components:

Key Components

  1. ICommandQueryBus: Mediator that routes commands/queries to handlers
  2. Commands: Requests that change system state
  3. Command Handlers: Process commands and coordinate domain logic
  4. Queries: Requests that retrieve data
  5. Query Handlers: Process queries and return data
  6. Domain Events: Published after successful command execution

Commands

Commands represent operations that change the state of the system. They express user intent and trigger business processes.

Command Interface

All commands implement one of these interfaces:
// Command without return value
public interface IRequestCommand : IRequest
{
}

// Command with return value
public interface IRequestCommand<out TResponse> : IRequest<TResponse>
{
}
Source: Core.Application.CommandQueryBus/Commands/IRequestCommand.cs:5-11

Command Example

public class CreateDummyEntityCommand : IRequestCommand<string>
{
    [Required]
    public string dummyPropertyOne { get; set; }
    public DummyValues dummyPropertyTwo { get; set; }

    public CreateDummyEntityCommand()
    {
    }
}
Source: Application/UseCases/DummyEntity/Commands/CreateDummyEntity/CreateDummyEntityCommand.cs:13-22

Command Characteristics

Commands use imperative names that express what should happen: CreateDummyEntityCommand, UpdateDummyEntityCommand, DeleteDummyEntityCommand.
Commands represent tasks or actions in the system, not just CRUD operations.
Can include data annotations for basic validation before reaching the handler.
Typically return IDs, success indicators, or result objects—not full entities.

More Command Examples

// Update command
public class UpdateDummyEntityCommand : IRequestCommand
{
    [Required]
    public string Id { get; set; }
    
    [Required]
    public string dummyPropertyOne { get; set; }
    
    public DummyValues dummyPropertyTwo { get; set; }
}

// Delete command
public class DeleteDummyEntityCommand : IRequestCommand
{
    [Required]
    public string Id { get; set; }
}

Command Handlers

Command Handlers process commands and orchestrate the domain logic to fulfill the request.

Command Handler Interface

// Handler without return value
public interface IRequestCommandHandler<in TRequest> : IRequestHandler<TRequest>
    where TRequest : IRequest
{
}

// Handler with return value
public interface IRequestCommandHandler<TRequest, TResponse> : IRequestHandler<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
}
Source: Core.Application.CommandQueryBus/Commands/IRequestCommandHandler.cs:5-13

Command Handler Example

internal sealed class CreateDummyEntityHandler : IRequestCommandHandler<CreateDummyEntityCommand, string>
{
    private readonly ICommandQueryBus _domainBus;
    private readonly IDummyEntityRepository _context;
    private readonly IDummyEntityApplicationService _dummyEntityApplicationService;

    public CreateDummyEntityHandler(
        ICommandQueryBus domainBus, 
        IDummyEntityRepository dummyEntityRepository, 
        IDummyEntityApplicationService dummyEntityApplicationService)
    {
        _domainBus = domainBus ?? throw new ArgumentNullException(nameof(domainBus));
        _context = dummyEntityRepository ?? throw new ArgumentNullException(nameof(dummyEntityRepository));
        _dummyEntityApplicationService = dummyEntityApplicationService ?? throw new ArgumentNullException(nameof(dummyEntityApplicationService));
    }

    public async Task<string> Handle(CreateDummyEntityCommand request, CancellationToken cancellationToken)
    {
        // 1. Create domain entity
        Domain.Entities.DummyEntity entity = new(request.dummyPropertyOne, request.dummyPropertyTwo);

        // 2. Validate domain entity
        if (!entity.IsValid) 
            throw new InvalidEntityDataException(entity.GetErrors());

        // 3. Check business rules
        if (_dummyEntityApplicationService.DummyEntityExist(entity.Id)) 
            throw new EntityDoesExistException();

        try
        {
            // 4. Persist entity
            object createdId = await _context.AddAsync(entity);

            // 5. Publish domain event
            await _domainBus.Publish(entity.To<DummyEntityCreated>(), cancellationToken);

            return createdId.ToString();
        }
        catch (Exception ex)
        {
            throw new BussinessException(ApplicationConstants.PROCESS_EXECUTION_EXCEPTION, ex.InnerException);
        }
    }
}
Source: Application/UseCases/DummyEntity/Commands/CreateDummyEntity/CreateDummyEntityHandler.cs:17-43

Command Handler Workflow

A typical command handler follows this pattern:
  1. Create/Retrieve Domain Entities: Instantiate or fetch domain objects
  2. Validate: Ensure domain entity is in a valid state
  3. Check Business Rules: Use application services to verify business constraints
  4. Persist Changes: Save to the repository
  5. Publish Events: Notify other parts of the system about what happened
  6. Return Result: Return success indicator or created ID
Command handlers should not return full entities. Return IDs, success flags, or result objects instead.

Queries

Queries retrieve data from the system without causing side effects. They are read-only operations.

Query Interface

// Query without return value (rare)
public interface IRequestQuery : IRequest
{
}

// Query with return value (typical)
public interface IRequestQuery<out TResponse> : IRequest<TResponse>
{
}
Source: Core.Application.CommandQueryBus/Queries/IRequestQuery.cs:5-11

Query Base Class

For paginated queries, inherit from QueryRequest<TResponse>:
public class QueryRequest<TResponse> : IRequestQuery<TResponse>
    where TResponse : class
{
    public uint PageIndex { get; set; }
    public uint PageSize { get; set; }
}
Source: Core.Application.CommandQueryBus/Queries/QueryRequest.cs:3-8

Query Examples

// Get all entities (paginated)
public class GetAllDummyEntitiesQuery : QueryRequest<QueryResult<DummyEntityDto>>
{
}
Source: Application/UseCases/DummyEntity/Queries/GetAllDummyEntities/GetAllDummyEntitiesQuery.cs:6-8
// Get single entity by ID
public class GetDummyEntityByQuery : IRequestQuery<DummyEntityDto>
{
    [Required]
    public string Id { get; set; }
}

Query Characteristics

Queries should never modify system state. They are purely read operations.
Queries return Data Transfer Objects (DTOs), not domain entities, to avoid exposing domain internals.
Use QueryRequest<T> and QueryResult<T> for paginated results.
Can include parameters for filtering, sorting, and searching.

Query Handlers

Query Handlers process queries and return data, typically mapped to DTOs.

Query Handler Interface

// Handler without return value
public interface IRequestQueryHandler<in TRequest> : IRequestHandler<TRequest>
    where TRequest : IRequest
{
}

// Handler with return value
public interface IRequestQueryHandler<TRequest, TResponse> : IRequestHandler<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
}
Source: Core.Application.CommandQueryBus/Queries/IRequestQueryHandler.cs:5-14

Query Handler Example

internal class GetAllDummyEntitiesHandler : IRequestQueryHandler<GetAllDummyEntitiesQuery, QueryResult<DummyEntityDto>>
{
    private readonly IDummyEntityRepository _context;

    public GetAllDummyEntitiesHandler(IDummyEntityRepository context)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
    }

    public async Task<QueryResult<DummyEntityDto>> Handle(GetAllDummyEntitiesQuery request, CancellationToken cancellationToken)
    {
        // 1. Retrieve entities from repository
        IList<Domain.Entities.DummyEntity> entities = await _context.FindAllAsync();

        // 2. Map to DTOs and return paginated result
        return new QueryResult<DummyEntityDto>(
            entities.To<DummyEntityDto>(), 
            entities.Count, 
            request.PageIndex, 
            request.PageSize);
    }
}
Source: Application/UseCases/DummyEntity/Queries/GetAllDummyEntities/GetAllDummyEntitiesHandler.cs:7-17

Query Result

The QueryResult<T> class provides paginated responses:
public class QueryResult<TEntity>
    where TEntity : class
{
    public long Count { get; private set; }
    public IEnumerable<TEntity> Items { get; private set; }
    public uint PageIndex { get; private set; }
    public uint PageSize { get; private set; }

    public QueryResult(IEnumerable<TEntity> items, long count, uint pageSize, uint pageIndex)
    {
        Items = items;
        Count = count;
        PageIndex = pageIndex;
        PageSize = pageSize;
    }
}
Source: Core.Application.CommandQueryBus/Queries/QueryResult.cs:3-17

Query Handler Workflow

  1. Retrieve Data: Fetch entities from repository
  2. Map to DTOs: Convert domain entities to data transfer objects
  3. Apply Pagination: Use PageIndex and PageSize from request
  4. Return Results: Return QueryResult with items and metadata
Query handlers should be lightweight and focused solely on data retrieval and mapping.

Command/Query Bus

The ICommandQueryBus is the central mediator that routes commands and queries to their handlers.

Interface

public interface ICommandQueryBus
{
    // Publish notifications (domain events)
    Task Publish<TNotification>(TNotification notification, CancellationToken cancellationToken = default) 
        where TNotification : INotification;
    
    // Send command/query without return value
    Task Send(IRequest request, CancellationToken cancellationToken = default);
    
    // Send command/query with return value
    Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default);
}
Source: Core.Application.CommandQueryBus/Buses/ICommandQueryBus.cs:5-10

Usage in API Controllers

[ApiController]
[Route("api/[controller]")]
public class DummyEntityController : ControllerBase
{
    private readonly ICommandQueryBus _bus;

    public DummyEntityController(ICommandQueryBus bus)
    {
        _bus = bus;
    }

    // POST: api/DummyEntity
    [HttpPost]
    public async Task<IActionResult> Create([FromBody] CreateDummyEntityCommand command)
    {
        var id = await _bus.Send(command);
        return CreatedAtAction(nameof(GetById), new { id }, id);
    }

    // GET: api/DummyEntity
    [HttpGet]
    public async Task<IActionResult> GetAll([FromQuery] GetAllDummyEntitiesQuery query)
    {
        var result = await _bus.Send(query);
        return Ok(result);
    }

    // GET: api/DummyEntity/{id}
    [HttpGet("{id}")]
    public async Task<IActionResult> GetById(string id)
    {
        var query = new GetDummyEntityByQuery { Id = id };
        var result = await _bus.Send(query);
        return Ok(result);
    }
}
The command/query bus provides a clean abstraction layer between controllers and business logic.

Domain Events and Notifications

After successful command execution, domain events can be published to trigger side effects.

Domain Event

public class DomainEvent : IRequestNotification
{
    public DateTime EventDateUtc { get; private set; }

    public DomainEvent()
    {
        EventDateUtc = DateTime.UtcNow;
    }
}
Source: Core.Application.CommandQueryBus/Notifications/DomainEvent.cs:3-10

Domain Event Example

internal sealed class DummyEntityCreated : DomainEvent
{
    public string Id { get; set; }
    public string DummyPropertyOne { get; set; }
    public DummyValues DummyPropertyTwo { get; set; }
}
Source: Application/DomainEvents/DummyEntityCreated.cs:10-15

Event Handler

internal class NotificateDummyEntityCreatedHandler : IRequestNotificationHandler<DummyEntityCreated>
{
    private readonly ILogger<NotificateDummyEntityCreatedHandler> _logger;

    public NotificateDummyEntityCreatedHandler(ILogger<NotificateDummyEntityCreatedHandler> logger)
    {
        _logger = logger;
    }

    public Task Handle(DummyEntityCreated notification, CancellationToken cancellationToken)
    {
        // Handle the event (e.g., send email, update cache, publish integration event)
        _logger.LogInformation($"DummyEntity created with ID: {notification.Id}");
        
        return Task.CompletedTask;
    }
}

Publishing Events

Events are published from command handlers:
// In command handler
await _domainBus.Publish(entity.To<DummyEntityCreated>(), cancellationToken);
Use domain events for in-process notifications and integration events for cross-service communication.

CQRS Best Practices

Command Best Practices

Each command should represent one business action or use case.
Validate commands at multiple levels: data annotations, application rules, and domain rules.
Consider making commands idempotent to handle retries safely.
Always publish domain events after successful state changes.

Query Best Practices

Queries must never modify state—they should be pure read operations.
Always return DTOs, never domain entities, to maintain encapsulation.
Optimize queries independently from commands (e.g., use read models, caching).

Handler Best Practices

Each command or query should have exactly one handler.
Use constructor injection for all dependencies.
Use specific exceptions for different error scenarios.
Use async operations for I/O-bound work (database, external APIs).

CQRS Flow Diagram

Command Flow

Query Flow


Advanced CQRS Patterns

Separate Read and Write Models

For high-scale applications, you can use different models for reads and writes:
  • Write Model: Domain entities with business logic
  • Read Model: Denormalized views optimized for queries
  • Synchronization: Use domain events to update read models

Event Sourcing

Combine CQRS with event sourcing:
  • Store events instead of current state
  • Rebuild state by replaying events
  • Natural audit trail
  • Time-travel debugging
Event sourcing adds complexity. Only use it when you have specific requirements for audit trails or temporal queries.

Testing CQRS Components

Testing Commands

[Fact]
public async Task CreateDummyEntity_ValidCommand_ReturnsId()
{
    // Arrange
    var command = new CreateDummyEntityCommand
    {
        dummyPropertyOne = "Test",
        dummyPropertyTwo = DummyValues.Value1
    };
    
    var mockRepo = new Mock<IDummyEntityRepository>();
    var mockBus = new Mock<ICommandQueryBus>();
    var mockService = new Mock<IDummyEntityApplicationService>();
    
    mockRepo.Setup(r => r.AddAsync(It.IsAny<DummyEntity>()))
            .ReturnsAsync("test-id");
    
    var handler = new CreateDummyEntityHandler(mockBus.Object, mockRepo.Object, mockService.Object);
    
    // Act
    var result = await handler.Handle(command, CancellationToken.None);
    
    // Assert
    Assert.NotNull(result);
    mockRepo.Verify(r => r.AddAsync(It.IsAny<DummyEntity>()), Times.Once);
    mockBus.Verify(b => b.Publish(It.IsAny<DummyEntityCreated>(), It.IsAny<CancellationToken>()), Times.Once);
}

Testing Queries

[Fact]
public async Task GetAllDummyEntities_ReturnsPagedResult()
{
    // Arrange
    var query = new GetAllDummyEntitiesQuery
    {
        PageIndex = 0,
        PageSize = 10
    };
    
    var mockRepo = new Mock<IDummyEntityRepository>();
    var entities = new List<DummyEntity>
    {
        new DummyEntity("value1", DummyValues.Value1),
        new DummyEntity("value2", DummyValues.Value2)
    };
    
    mockRepo.Setup(r => r.FindAllAsync()).ReturnsAsync(entities);
    
    var handler = new GetAllDummyEntitiesHandler(mockRepo.Object);
    
    // Act
    var result = await handler.Handle(query, CancellationToken.None);
    
    // Assert
    Assert.Equal(2, result.Count);
    Assert.Equal(2, result.Items.Count());
}

Next Steps

Create Commands

Learn how to implement commands and handlers

Create Queries

Build queries for data retrieval

Domain Events

Work with domain events and notifications

API Controllers

Expose commands and queries via REST API

Build docs developers (and LLMs) love