Showing posts with label CQRS .NET Core. Show all posts
Showing posts with label CQRS .NET Core. Show all posts

Tuesday, June 30, 2026

CQRS (Command Query Responsibility Segregation) in .NET: A Complete Guide with Real-World Banking Example

Introduction

As enterprise applications grow, handling millions of users while maintaining performance becomes increasingly challenging. A traditional CRUD architecture, where the same model is responsible for both reading and writing data, often becomes a bottleneck.

Imagine a banking application where thousands of customers are transferring money, depositing funds, and withdrawing cash, while millions of users are simultaneously checking their account balances and transaction history. Using the same model and database operations for both reads and writes can lead to scalability issues, complex business logic, and poor performance.

This is where CQRS (Command Query Responsibility Segregation) comes into play.

CQRS is an architectural pattern that separates read operations from write operations, allowing each side of the application to be designed, optimized, and scaled independently.

In this article, we'll explore CQRS from the ground up, understand its architecture, implement it using ASP.NET Core, and examine how it solves real-world enterprise challenges.


What is CQRS?

CQRS stands for Command Query Responsibility Segregation.

The idea behind CQRS is surprisingly simple:

  • Commands modify data.

  • Queries retrieve data.

Instead of using a single model for both operations, CQRS separates them into two independent models.

In a traditional CRUD application, the same entity is responsible for both reading and updating data.

Client
   |
Application
   |
Database

Everything goes through the same model.

With CQRS, the architecture becomes:

                Client

         /                 \

     Commands            Queries

         |                   |

    Write Model        Read Model

         |                   |

    Write Database     Read Database

The write side focuses on business rules and consistency, while the read side is optimized for speed and data retrieval.


Why Do We Need CQRS?

Consider a real-world banking system.

Every day, customers perform operations such as:

  • Depositing money

  • Withdrawing money

  • Transferring funds

  • Opening new accounts

These operations modify data.

At the same time, customers constantly check:

  • Account balance

  • Transaction history

  • Mini statements

  • Monthly statements

These operations only read data.

In most banking applications, read operations are significantly higher than write operations.

Example:

OperationDaily Requests
Check Balance1,000,000
View Transactions650,000
Transfer Money35,000
Deposit Money18,000
Withdraw Money20,000

Clearly, the application spends much more time serving read requests.

CQRS allows us to optimize both workloads independently.


Understanding Commands

A Command represents an intention to change the application's state.

Examples include:

  • Create Customer

  • Place Order

  • Deposit Money

  • Withdraw Money

  • Cancel Order

  • Update Address

A command should never return business data.

For example:

Deposit ₹5,000

Expected response:

Success

Not:

Current Balance = ₹25,000

If the client needs the updated balance, it should execute a separate query.

This separation keeps responsibilities clean and predictable.


Understanding Queries

Queries are responsible only for retrieving information.

Examples include:

  • Get Customer Details

  • Get Balance

  • Search Products

  • View Orders

  • Transaction History

Queries must never modify data.

Their sole responsibility is to return information in the most efficient way possible.


Real-World Banking Example

Let's build a simple banking scenario.

John currently has:

Balance = ₹10,000

He deposits ₹5,000.

The client sends:

POST /accounts/deposit

Request:

{
  "accountId": 101,
  "amount": 5000
}

The request follows this flow:

API

↓

Deposit Command

↓

Command Handler

↓

Domain Logic

↓

Repository

↓

SQL Server

After the command completes, the balance becomes:

₹15,000

Now the user checks the balance.

GET /accounts/101

This request follows a different path.

API

↓

GetBalance Query

↓

Query Handler

↓

Read Database

↓

DTO

↓

Response

The response is:

{
  "balance": 15000
}

Notice that the read operation never touches the write model.


Why Separate Read and Write Models?

The write side contains business rules.

For example:

Account

- Account Number
- Balance
- Domain Events
- Version
- Validation Rules

The read side only contains information required by the user interface.

AccountSummary

- Customer Name
- Current Balance
- Last Transaction
- Reward Points

There is no unnecessary logic.

This makes queries extremely fast.


CQRS in ASP.NET Core

A typical project structure looks like this:

Application

 ├── Commands
 │      ├── DepositMoney
 │      ├── WithdrawMoney
 │      └── TransferMoney
 │
 ├── Queries
 │      ├── GetBalance
 │      ├── GetStatement
 │      └── GetTransactions
 │
 ├── Validators
 │
 ├── Domain
 │
 ├── Infrastructure
 │
 └── API

This structure keeps responsibilities organized and easy to maintain.


Implementing a Command

A command represents a user's intention.

public class DepositMoneyCommand : IRequest
{
    public int AccountId { get; init; }

    public decimal Amount { get; init; }
}

The command handler performs the business logic.

public class DepositMoneyHandler : IRequestHandler<DepositMoneyCommand>
{
    private readonly IAccountRepository repository;

    public DepositMoneyHandler(IAccountRepository repository)
    {
        this.repository = repository;
    }

    public async Task Handle(
        DepositMoneyCommand request,
        CancellationToken cancellationToken)
    {
        var account = await repository.GetAsync(request.AccountId);

        account.Deposit(request.Amount);

        await repository.SaveAsync(account);
    }
}

Notice that the controller contains almost no business logic.


Implementing a Query

The query only retrieves information.

public class GetBalanceQuery : IRequest<AccountDto>
{
    public int AccountId { get; init; }
}

The handler is simple.

public class GetBalanceHandler
    : IRequestHandler<GetBalanceQuery, AccountDto>
{
    private readonly IReadRepository repository;

    public GetBalanceHandler(IReadRepository repository)
    {
        this.repository = repository;
    }

    public async Task<AccountDto> Handle(
        GetBalanceQuery request,
        CancellationToken cancellationToken)
    {
        return await repository.GetBalanceAsync(request.AccountId);
    }
}

There is no business logic because queries should only retrieve data.


CQRS with MediatR

MediatR is the most popular library used with CQRS in ASP.NET Core.

Instead of calling services directly, controllers send commands and queries through the mediator.

[HttpPost]
public async Task<IActionResult> Deposit(
    DepositMoneyCommand command)
{
    await _mediator.Send(command);

    return Ok();
}

Similarly,

[HttpGet("{id}")]
public async Task<AccountDto> Get(int id)
{
    return await _mediator.Send(
        new GetBalanceQuery
        {
            AccountId = id
        });
}

This keeps controllers thin and promotes loose coupling.


CQRS in an E-Commerce System

Consider an online shopping platform.

When a customer places an order, the workflow is complex.

Place Order

↓

Validate Inventory

↓

Reserve Stock

↓

Calculate Discounts

↓

Process Payment

↓

Create Order

↓

Publish Event

However, retrieving order details is much simpler.

Get Order

↓

Read Database

↓

Return DTO

Different responsibilities deserve different models.


CQRS with Event-Driven Architecture

Enterprise systems often combine CQRS with messaging.

Deposit Money

↓

MoneyDeposited Event

↓

Azure Service Bus

↓

Notification Service

↓

Reporting Service

↓

Analytics Service

Each service reacts independently without tightly coupling business logic.

This architecture improves scalability and resilience.


Eventual Consistency

One important concept in CQRS is eventual consistency.

Suppose the write database updates immediately.

Write Database

↓

Publish Event

↓

Read Database Updated

There may be a short delay before the read database reflects the latest changes.

This delay is acceptable for most enterprise systems because the read model eventually becomes consistent.


Advantages of CQRS

Improved Performance

Read operations are optimized independently from write operations.


Independent Scaling

If your application receives millions of read requests but only thousands of writes, you can scale only the read infrastructure.


Cleaner Business Logic

Business rules remain isolated within command handlers and domain models.


Better Security

Commands can require stricter authorization while queries may expose public information safely.


Easier Maintenance

Separating responsibilities makes large applications easier to understand and maintain.


Better Reporting

Read databases can be denormalized for analytics without impacting transactional performance.


Challenges of CQRS

CQRS is powerful but introduces additional complexity.

Some common challenges include:

  • More project structure

  • Additional handlers

  • Eventual consistency

  • Messaging infrastructure

  • Synchronizing read models

  • Increased deployment complexity

For small CRUD applications, CQRS may be unnecessary.


When Should You Use CQRS?

CQRS is an excellent choice when your application has:

  • Complex business rules

  • High read-to-write ratio

  • Microservices architecture

  • Event-driven workflows

  • Large enterprise domains

  • Independent scaling requirements

  • High-performance reporting needs


When Should You Avoid CQRS?

CQRS may not be suitable for:

  • Simple CRUD applications

  • Small internal tools

  • Basic admin portals

  • MVPs and prototypes

  • Applications with minimal business logic

In these cases, a traditional layered architecture is often simpler and more cost-effective.


Best Practices

When implementing CQRS, follow these guidelines:

  • Keep commands focused on a single business action.

  • Keep queries read-only.

  • Use immutable DTOs.

  • Validate commands using FluentValidation.

  • Keep controllers thin.

  • Place business rules inside the domain layer.

  • Publish domain events after successful commands.

  • Use asynchronous messaging for updating read models.

  • Cache frequently used queries using Redis.

  • Monitor command failures and message retries.


CQRS with Clean Architecture

CQRS fits naturally into Clean Architecture.

Presentation Layer

↓

API Controllers

↓

MediatR

↓

Command / Query Handlers

↓

Domain Layer

↓

Infrastructure

↓

SQL Server

↓

Azure Service Bus

↓

Read Database

This architecture promotes loose coupling, testability, scalability, and maintainability.


Frequently Asked Interview Questions

What is CQRS?

CQRS is an architectural pattern that separates write operations (Commands) from read operations (Queries), allowing each side to evolve independently.


Does CQRS require two databases?

No.

Many applications start with a single database while maintaining separate command and query models. Separate databases can be introduced later if needed.


Is CQRS the same as Event Sourcing?

No.

CQRS separates reads and writes.

Event Sourcing stores every state change as an event.

They are often used together but solve different problems.


Why is MediatR commonly used?

MediatR decouples controllers from business logic by routing commands and queries to their appropriate handlers.


What is eventual consistency?

It means the read model may not reflect the latest write immediately, but it will become consistent after asynchronous processing completes.


Final Thoughts

CQRS is not just another design pattern—it is a strategic architectural approach that enables applications to scale efficiently while keeping business logic organized and maintainable.

By separating reads from writes, organizations can independently optimize each workload, simplify domain logic, improve performance, and build systems capable of handling enterprise-scale traffic.

When combined with Clean Architecture, Domain-Driven Design (DDD), MediatR, Event-Driven Architecture, Azure Service Bus, RabbitMQ, and Microservices, CQRS becomes one of the most effective patterns for developing modern cloud-native .NET applications.

Like any architectural pattern, CQRS should be applied where it adds value. For complex enterprise systems with demanding scalability and business requirements, it is an excellent choice. For smaller CRUD applications, however, the additional complexity may not be justified.

Understanding where and how to apply CQRS is what separates experienced software architects from developers who simply follow trends. Use it thoughtfully, and it can become a cornerstone of building robust, scalable, and maintainable enterprise applications.

Sunday, September 28, 2025

Implementing Microservices with .NET Core and Angular

 Using (CQRSSagaAzure Service BusCircuit Breaker • Adapter • Patterns & Best Practices) 

TL;DR (What you'll build)

A cloud-ready microservices system where each microservice:

  • Is a .NET Core Web API (REST) exposing bounded-capability endpoints.

  • Uses CQRS: separate read model and write model (commands vs queries).

  • Coordinates complex business workflows using SAGA (via Azure Service Bus).

  • Communicates async via Azure Service Bus (topics/subscriptions or queues).

  • Uses Circuit Breaker (Polly) + retry/backoff to improve resilience.

  • Uses Adapter pattern for external systems and for infrastructure abstraction.

  • Front-end is an Angular SPA calling an API Gateway (or BFF) that routes to microservices.


Architecture overview (high level)

  1. API Gateway / BFF (optional): Single public endpoint, routing, auth, aggregation.

  2. Microservices (multiple): each with its own database (DB-per-service), own bounded context.

    • Example services: Orders, Payments, Inventory, Customers, Shipping.

  3. Azure Service Bus: topics for publish/subscribe events, queues for commands/tasks.

  4. CQRS: Commands handled by write-side; queries served by optimized read DB (e.g., read replica or denormalized store like CosmosDB/SQL).

  5. Sagas: Orchestrate long-running transactions (compensations) via events/messages.

  6. Polly: Circuit Breaker + Retry + Timeout for outbound HTTP/Bus calls.

  7. Monitoring & Logging: Application Insights, centralized logs (ELK/Serilog sink).

  8. CI/CD & Hosting: Build pipeline -> container images -> AKS / App Service.

Diagram (textual):

Angular SPA -> API Gateway -> Microservice A (Commands) -> Azure Service Bus topic -> Microservice B subscribes (Updates read DB) Microservice A -> publishes events -> Azure Service Bus -> Saga Orchestrator -> other services

Core design principles

  • Single Responsibility & Bounded Contexts — keep microservices small and focused.

  • DB-per-service — avoid shared DB to reduce coupling.

  • Event-driven — communicate asynchronously where eventual consistency is acceptable.

  • Idempotency — message handlers must be idempotent.

  • Id-based correlation — include correlation IDs in messages for tracing.

  • Resilience — use bulkheads, circuit breakers, retries, timeouts.

  • Observability — structured logs, distributed tracing (W3C TraceContext).


Key patterns & how to apply them

1. CQRS (Command Query Responsibility Segregation)

  • Write side: handles commands (POST /ordersCreateOrderCommand). Validations and state changes happen here.

  • Read side: optimized projection used for queries (GET /orders/123). Read DB is updated by event subscribers.

  • Implementation:

    • Commands go to controllers → Command Handlers (MediatR is common).

    • After state change, publish domain event (to Azure Service Bus).

    • Event consumers update read model (denormalized tables or NoSQL).

Example files/folders:

/OrdersService /Commands CreateOrderCommand.cs CreateOrderCommandHandler.cs /Domain Order.cs OrderAggregate.cs /Events OrderCreatedEvent.cs /ReadModel OrderReadModel.cs OrderProjectionHandler.cs

2. Saga (long-running transactions)

  • Use Saga to coordinate multi-step flows (e.g., order -> reserve inventory -> charge payment -> ship).

  • Two approaches:

    • Choreography: each service reacts to events. Simple but can be hard to reason about for complex flows.

    • Orchestration: Saga Orchestrator service sends commands to participants and listens for replies/events. Easier to visualize and control compensations.

  • Implementation with Azure Service Bus:

    • Orchestrator publishes ReserveInventoryCommand to a queue.

    • Inventory service replies InventoryReservedEvent or InventoryReserveFailedEvent.

    • Orchestrator then issues ChargePaymentCommand or CompensateOrderCommand.

  • Always plan for compensation (rollback-like steps) when a later step fails.

3. Azure Service Bus (Messaging)

  • Use Topics + Subscriptions for publish/subscribe; Queues for point-to-point commands.

  • Message schema:

    • MessageId, CorrelationId, CausationId, MessageType, Payload.

  • Use dead-letter queues for poison messages and implement retries & DLQ handling.

4. Circuit Breaker & Resilience (Polly)

  • Wrap all outbound network calls (HTTP, DB, Service Bus operations) with retry + jittered backoff + circuit breaker.

  • Typical policy: Retry (3 attempts, exponential backoff) + Circuit Breaker (open after 5 failures for 30s) + Timeout (5s).

  • Example: use HttpClientFactory with Polly policies.

5. Adapter Pattern

  • Use adapters to isolate external/specific client libs (e.g., Azure Service Bus SDK, Payment Gateway SDK) from your domain.

  • Define an interface (e.g., IPaymentGateway) and implement StripeAdapter/DummyAdapter. This supports testability and swapping providers.

6. Repository + Unit of Work (when needed)

  • Use repository for aggregate persistence; Unit of Work if multiple repositories within same DB transaction (but prefer small transactions).

7. Anti-Corruption Layer

  • When integrating with legacy systems, use an adapter/anti-corruption layer to map incompatible models.


Concrete implementation steps

1. Set up projects

Solution example:

/MyEshop /src /ApiGateway (aspnet core) /Services /Orders.Api /Inventory.Api /Payments.Api /Saga.Orchestrator /Shared /Messaging (contracts) /Common (logging, exceptions) /ui /angular-spa

2. Shared contracts & DTOs

Create a Shared/Messaging project with message types (events/commands) used across services. Strongly-typed messages reduce coupling.

Example OrderCreatedEvent:

public record OrderCreatedEvent(Guid OrderId, Guid CustomerId, decimal Total, IEnumerable<OrderLineDto> Lines);

3. Implement CQRS in a service (Orders example)

  • Use MediatR for intra-service command/handler pattern.

  • Controller:

[HttpPost] public async Task<IActionResult> Create([FromBody] CreateOrderRequest req) { var cmd = new CreateOrderCommand(req.CustomerId, req.Lines); var result = await _mediator.Send(cmd); return CreatedAtAction(..., result.OrderId); }
  • CommandHandler:

public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, CreateOrderResult> { private readonly IOrderRepository _repo; private readonly IMessageBus _bus; // adapter over Azure Service Bus public async Task<CreateOrderResult> Handle(CreateOrderCommand request, CancellationToken token) { var order = Order.Create(request.CustomerId, request.Lines); await _repo.AddAsync(order); await _repo.UnitOfWork.SaveChangesAsync(); // publish event to bus await _bus.Publish(new OrderCreatedEvent(order.Id, order.CustomerId, order.Total, order.Lines)); return new CreateOrderResult(order.Id); } }

4. Publish/Subscribe (Azure Service Bus)

  • Implement IMessageBus adapter that wraps Azure Service Bus ServiceBusClient.

  • For publishing events:

public async Task Publish<T>(T @event) where T : class { var message = JsonSerializer.Serialize(@event); await _sender.SendMessageAsync(new ServiceBusMessage(message) { MessageId = Guid.NewGuid().ToString() }); }
  • For subscriptions, create background services (hosted services) to process messages and project read model.

5. Saga Orchestration (example flow)

  • Orders publishes OrderCreatedEvent.

  • Saga.Orchestrator subscribes. On OrderCreated:

    1. Send ReserveInventoryCommand to Inventory queue.

    2. Wait for InventoryReservedEvent or InventoryReserveFailedEvent.

    3. If reserved, send ChargePaymentCommand to Payments.

    4. If payment succeeds, send ShipOrderCommand to Shipping.

    5. If any step fails, run compensating actions (e.g., ReleaseInventoryCommand, RefundPaymentCommand) and mark order Failed.

Orchestrator pattern: implement state persistence for saga instances (e.g., in CosmosDB/SQL). Store Saga state keyed by CorrelationId.

6. Circuit Breaker with HttpClientFactory + Polly

Register in Program.cs:

services.AddHttpClient("payments", client => { client.BaseAddress = new Uri(config["Payments:BaseUrl"]); }) .AddPolicyHandler(GetRetryPolicy()) .AddPolicyHandler(GetCircuitBreakerPolicy()); IAsyncPolicy<HttpResponseMessage> GetRetryPolicy() => HttpPolicyExtensions.HandleTransientHttpError() .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy() => HttpPolicyExtensions.HandleTransientHttpError() .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));

7. Angular front-end (simple BFF pattern)

  • Angular calls API Gateway/BFF endpoints which route to microservices.

  • Use typed services, interceptors for auth (JWT) and correlation headers.

Example Angular service:

@Injectable({ providedIn: 'root' }) export class OrdersService { constructor(private http: HttpClient) {} createOrder(order: CreateOrderDto) { return this.http.post<ApiResponse<OrderView>>('/api/orders', order); } }

Add an HTTP interceptor to send x-correlation-id header and Authorization bearer token.

8. Idempotency & Message deduplication

  • Keep ProcessedMessage table with MessageId to ignore duplicate message deliveries.

  • Use Service Bus message-id + dedupe logic.

9. Testing

  • Unit test command handlers and domain logic.

  • Integration tests for message flows (use in-memory or test instance of Service Bus or use the Azure Service Bus emulator).

  • Contract tests for message contracts.

10. Observability, Monitoring & Health

  • Use Application Insights (or OpenTelemetry) for distributed tracing and metrics.

  • Add Health Checks endpoints and Kubernetes liveness/readiness probes.

  • Structured logging with Serilog, sink to central store.


Deployment & CI/CD (short)

  • Dockerize each service.

  • Build pipeline: build -> test -> image -> push to ACR.

  • Deploy pipeline: pull image -> AKS (Kubernetes manifests) or Azure App Service.

  • Use Helm charts for Kubernetes; include config via ConfigMaps and secrets via Azure Key Vault.

  • Use canary / blue-green with ingress controller and feature flags (LaunchDarkly or open-source).


Practical tips & pitfalls

  • Do not share a single database across services — this creates high coupling.

  • Prefer eventual consistency; design UIs to show pending states (e.g., "Payment Processing").

  • Compensation is not rollback — design compensating actions carefully.

  • Keep messages small and versionable — maintain backward compatibility.

  • Start with choreography for simple flows; move to orchestrator for complex flows requiring centralized control.

  • Use feature toggles while deploying new message flows.


Minimal code sample: publish event to Service Bus (adapter)

public interface IMessageBus { Task PublishAsync<T>(T @event, string topicName); } public class AzureServiceBusAdapter : IMessageBus { private readonly ServiceBusClient _client; public AzureServiceBusAdapter(ServiceBusClient client) => _client = client; public async Task PublishAsync<T>(T @event, string topicName) { var sender = _client.CreateSender(topicName); var body = JsonSerializer.Serialize(@event); var msg = new ServiceBusMessage(body) { MessageId = Guid.NewGuid().ToString() }; msg.ApplicationProperties["MessageType"] = typeof(T).FullName; await sender.SendMessageAsync(msg); } }

Folder structure suggestion (service-level)

/Orders.Api /Controllers /Commands /CommandHandlers /Domain /Events /Projections /Infrastructure /Persistence /Messaging (ServiceBus adapter) /HttpClients (adapters) /Services (business services) /Tests

Checklist before go-live

  • Message contracts versioned & documented.

  • Idempotency & DLQ handled.

  • Circuit breakers instrumented.

  • Health checks + readiness gates.

  • Logging and distributed tracing configured.

  • Saga persistence & compensations tested.

  • Secure secrets (Key Vault) and communication (TLS, JWT).


Suggested next steps (practical small milestones)

  1. Create a Shared.Messaging project with event/command contracts.

  2. Scaffold Orders service with basic create order command and publisher (no Saga yet).

  3. Create Inventory service subscriber to update read model.

  4. Add Azure Service Bus adapter and test end-to-end message flow locally (or in dev subscription).

  5. Implement a simple saga orchestrator for a 3-step flow (inventory → payment → shipping).

  6. Add Polly policies and instrument logging/tracing.

  7. Dockerize and deploy to a dev cluster.


Don't Copy

Protected by Copyscape Online Plagiarism Checker