Showing posts with label Architecture. Show all posts
Showing posts with label Architecture. Show all posts

Friday, November 21, 2025

Architecture - gRPC (General Remote Procedure Call) Framework

gRPC is a high-performance, open-source, RPC (Remote Procedure Call) framework developed by Google.

It allows services to communicate directly with each other using strongly typed contracts (.proto files) over a fast binary protocol (HTTP/2).

In simple terms:

gRPC allows one service to call a method in another service as if it were calling a local function, but over the network.


 


๐Ÿงฑ Key Components of gRPC

ComponentPurpose
Protocol Buffers (Protobuf)Language-neutral binary serialization format
.proto fileDefines contracts (messages + service methods)
gRPC client & server stubsAuto-generated code based on .proto
HTTP/2Transport protocol (multiplexing, streaming)

⚡ Why is gRPC so Fast?

๐Ÿš€ Because it uses:

  1. Binary protocol (Protocol Buffers) → smaller & faster than JSON
  2. HTTP/2 → multiplexing, header compression
  3. Streaming built-in
  4. Strongly typed contracts

Result:

  1. Super low latency
  2. High throughput
  3. Efficient for microservices

๐ŸŸข gRPC vs REST

FeaturegRPCREST
ProtocolHTTP/2HTTP/1.1
Data FormatProtobuf (binary)JSON (text)
SpeedFasterSlower
ContractStrong, auto-generatedOptional (Swagger)
StreamingFull support (4 types)Limited
Browser supportWeak (needs gRPC-Web)Native

✳️ When to Use gRPC

Use gRPC when you need:
✔ High performance
✔ Service-to-service communication
✔ Real-time communication
✔ Low latency mobile/IoT calls
✔ Strong contract enforcement

Not ideal for:
❌ Public APIs
❌ Browser-to-server calls (requires gRPC-web wrapper)


๐ŸŽฏ Types of gRPC Calls

TypeDescription
UnarySingle request → single response
Server streamingClient sends 1 request → server streams responses
Client streamingClient streams requests → server sends 1 response
Bi-directional streamingBoth sides stream simultaneously

๐Ÿ“„ Example .proto File

syntax = "proto3"; service OrderService { rpc GetOrder (OrderRequest) returns (OrderResponse); rpc StreamOrders (OrderRequest) returns (stream OrderResponse); } message OrderRequest { int32 orderId = 1; } message OrderResponse { int32 orderId = 1; string status = 2; }

๐Ÿงฉ C# Server Implementation

public class OrderServiceImpl : OrderService.OrderServiceBase { public override Task<OrderResponse> GetOrder(OrderRequest request, ServerCallContext context) { return Task.FromResult(new OrderResponse { OrderId = request.OrderId, Status = "Processing" }); } }

๐Ÿงฉ C# Client Implementation

var channel = GrpcChannel.ForAddress("https://localhost:5001"); var client = new OrderService.OrderServiceClient(channel); var response = await client.GetOrderAsync(new OrderRequest { OrderId = 1 }); Console.WriteLine(response.Status);

๐Ÿงฒ Where gRPC is Used (Real World)

  1. Google’s internal microservices
  2. Netflix → high-performance service communication
  3. Uber → low-latency RPC
  4. Dropbox
  5. Cloud Native (Kubernetes uses gRPC internally)

๐Ÿ–ฅ In .NET Ecosystem

.NET has first-class gRPC support, including:

  1. gRPC hosting via ASP.NET Core
  2. gRPC client library
  3. gRPC-Web
  4. Protobuf serialization
  5. Interceptors (similar to middleware)


๐Ÿง  Simple Interview-Perfect Definition

gRPC is a high-performance RPC framework using HTTP/2 and Protocol Buffers that enables fast, efficient communication between microservices with strongly typed contracts and support for streaming.


Thursday, November 20, 2025

Architecture - Saga Design Pattern

"Saga" is a distributed transaction pattern used in microservices to ensure data consistency without using 2-phase commit or distributed locks.

✔ In simpler words:

Saga ensures that a long-running business process composed of multiple microservice operations either completes successfully OR compensates (undoes) the already completed steps.


 

๐Ÿš€ Use Saga when:

  • A single business operation touches multiple microservices, each with its own DB.
  • You cannot use distributed transactions (like ACID/2PC).
  • You need an eventual consistency model.

๐ŸŽฏ Why Saga Is Needed

Typical distributed system problems:

  • Every service owns its database → no cross-service transaction possible.
  • Operations may fail in the middle.
  • You must prevent partial success (e.g., payment done but inventory not reserved).

Traditional ACID transactions do not work across services—Saga solves this using compensating actions.


๐Ÿง  Saga Pattern Key Principles

  1. A business workflow is broken into multiple local transactions.
  2. Each local transaction:
    • Performs a step.
    • Publishes an event.
  3. If any step fails → Saga triggers compensation (undo logic).

๐Ÿ”„ Two Types of Saga: Orchestration vs Choreography


๐Ÿ…ฐ️ 1. Choreography-Based Saga (Event-Driven)

Services talk to each other using events.


✔ Flow example:

  1. Order Service → "OrderCreated" event
  2. Inventory Service → reserves stock → "StockReserved"
  3. Payment Service → charges customer → "PaymentSuccess"
  4. Shipping Service → ships product → "OrderShipped"

If failure:

  • Payment fails → publish "PaymentFailed"
  • Inventory Service → receives and releases reserved stock
  • Order Service → marks order as cancelled

✔ Pros

  • No central coordinator
  • Simple for small flows
  • Very loosely coupled

❌ Cons

  • Hard to visualize / debug
  • Complex chains of events → “distributed spaghetti
  • Hard to evolve as workflow grows

๐Ÿ…ฑ️ 2. Orchestration-Based Saga

A central Saga Orchestrator commands each step.


✔ Flow example:

  1. Orchestrator → ReserveInventory()
  2. Orchestrator → ChargePayment()
  3. Orchestrator → CreateShipment()

If failure:

  • Orchestrator calls CompensateInventory(), RefundPayment(), etc.

✔ Pros

  • Centralized logic → easier to understand
  • Traceable
  • Easier for complex workflows

❌ Cons

  • Orchestrator becomes the “brain” → slightly more coupling


๐Ÿ“ฆ Real Example: E-Commerce Order Workflow

Step-by-step:

  1. Order Service
    • Creates order
    • Saga starts
  2. Inventory Service
    • Reserves stock
  3. Payment Service
    • Charges customer
  4. Shipping Service
    • Schedules delivery
  5. Saga completes

If Payment fails:

Saga automatically:

  • Cancels shipment
  • Releases stock
  • Cancels order

๐Ÿงฐ C# Example — Orchestration Saga Using MassTransit

public class OrderState : SagaStateMachineInstance { public Guid CorrelationId { get; set; } public string CurrentState { get; set; } public Guid OrderId { get; set; } } public class OrderStateMachine : MassTransitStateMachine<OrderState> { public State InventoryReserved { get; private set; } public Event<OrderCreated> OrderCreatedEvent { get; private set; } public OrderStateMachine() { InstanceState(x => x.CurrentState); Event(() => OrderCreatedEvent, x => x.CorrelateById(context => context.Message.OrderId)); Initially( When(OrderCreatedEvent) .Then(context => { Console.WriteLine("Order Created → Reserving inventory..."); }) .Publish(context => new ReserveInventory { OrderId = context.Instance.OrderId }) .TransitionTo(InventoryReserved) ); // Additional states + transitions omitted for brevity } }

This orchestrates:

  • Reserve inventory
  • Process payment
  • Ship order
  • Handle compensation

๐Ÿงฐ C# Example — Choreography Saga (Event-Driven)

Order Service publishes event:

await _publishEndpoint.Publish(new OrderCreated(orderId));

Inventory handles event:

public class OrderCreatedHandler : IConsumer<OrderCreated> { public async Task Consume(ConsumeContext<OrderCreated> context) { // Local transaction ReserveStock(context.Message.OrderId); await context.Publish(new StockReserved { OrderId = context.Message.OrderId }); } }

Payment handles event:

public class StockReservedHandler : IConsumer<StockReserved> { public async Task Consume(ConsumeContext<StockReserved> context) { var result = Charge(context.Message.OrderId); if (result) await context.Publish(new PaymentCompleted { OrderId = context.Message.OrderId }); else await context.Publish(new PaymentFailed { OrderId = context.Message.OrderId }); } }

๐Ÿงจ Compensating Transaction Example

public class PaymentFailedHandler : IConsumer<PaymentFailed> { public async Task Consume(ConsumeContext<PaymentFailed> context) { await context.Publish(new ReleaseStock { OrderId = context.Message.OrderId }); await context.Publish(new CancelOrder { OrderId = context.Message.OrderId }); } }

๐ŸŽฏ When to Use Saga

Use Saga when:

  • Business processes affect multiple services.
  • Rollbacks must be handled safely.
  • Workflow is long-running (minutes → hours).
  • Consistency between services is required.

Don't use Saga when:

  • Single service owns all data.
  • Strong ACID guarantees needed.
  • Steps cannot be compensated.

Tuesday, November 18, 2025

Architecture - Some Important Concepts of DDD

1. Domain Events

Domain events model things that happen inside the domain and allow other parts of the system to react.

Use cases

  • Send email after order placed
  • Reduce inventory
  • Publish message to message bus
  • Update read models

Domain Event Example

public class OrderPlacedEvent : IDomainEvent { public Guid OrderId { get; } public DateTime PlacedAt { get; } public OrderPlacedEvent(Guid orderId) { OrderId = orderId; PlacedAt = DateTime.UtcNow; } }

Aggregate raising event

public class Order : AggregateRoot { public List<OrderItem> Items { get; private set; } public void Place() { // business logic... AddDomainEvent(new OrderPlacedEvent(Id)); } }

Domain Event Handler

public class OrderPlacedHandler : IHandler<OrderPlacedEvent> { public Task Handle(OrderPlacedEvent domainEvent) { // Example action Console.WriteLine($"Order {domainEvent.OrderId} placed!"); return Task.CompletedTask; } }

2. Ubiquitous Language

It means:

Everyone (developers, business, testers, PMs) uses the SAME terms with SAME meaning.

Example:
Business says Order → OrderItem → Payment → Customer
So your code should also use Order, not “Tbl_Order”.

Bad example:

  • Business says “Order”

  • Code says “PurchaseTransactionRecord”

Good example:

public class Order { } public class OrderItem { }

This maintains clarity and reduces misunderstandings.


3. Bounded Context

A bounded context defines a logical boundary within which a domain model is consistent.

Example:

In an E-commerce system:

Bounded ContextMeaning of “Order”
Ordering BCOrder = items customer wants
Billing BCOrder = financial transaction
Shipping BCOrder = package to be shipped

Each BC has its own:

  • Model
  • Database
  • Services
  • Ubiquitous language

C# Example

Different BCs have different Order definitions:

Ordering BC

public class Order { public Guid Id { get; } public List<OrderItem> Items { get; } }

Shipping BC

public class OrderShipment { public Guid OrderId { get; } public string TrackingNumber { get; private set; } }

4. DTO (Data Transfer Object)

DTOs are simple objects used to transfer data across boundaries (API layer ↔ Application layer).

Not used for business logic.

Example

public class OrderDto { public Guid Id { get; set; } public decimal TotalAmount { get; set; } }

Controller returning DTO instead of domain entity

[HttpGet("{id}")] public async Task<ActionResult<OrderDto>> GetOrder(Guid id) { var order = await _orderService.GetOrder(id); return new OrderDto { Id = order.Id, TotalAmount = order.TotalAmount }; }

5. Aggregate

Aggregate = cluster of domain objects treated as a single unit
Aggregate Root = main entry point, enforces invariants.

Example

Order (root) → contains OrderItems (child entities)

public class Order : AggregateRoot { private readonly List<OrderItem> _items = new(); public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly(); public void AddItem(Guid productId, int qty, decimal price) { _items.Add(new OrderItem(productId, qty, price)); } }

Child entity:

public class OrderItem { public Guid ProductId { get; } public int Quantity { get; private set; } public decimal Price { get; private set; } internal OrderItem(Guid productId, int qty, decimal price) { ProductId = productId; Quantity = qty; Price = price; } }

Rules:

  • Only aggregate root is loaded from repository.
  • Only aggregate root is saved.
  • External code cannot modify inner entities directly.


6. Value Object

Value objects:

  • Immutable

  • Compared by values (not identity)

  • No Id

  • Represent concepts like Money, Email, Address, Quantity

C# Example

public class Email : ValueObject { public string Value { get; } public Email(string email) { if (!email.Contains("@")) throw new Exception("Invalid email"); Value = email; } protected override IEnumerable<object> GetEqualityComponents() { yield return Value; } }

Usage:

var userEmail = new Email("test@gmail.com");

7. Rich Model vs Anemic Model

Anemic Model (Anti-pattern)

  • Entities only have properties
  • Logic sits outside in service classes
  • Looks like a database table
  • Easy but poor domain modeling

Example:

public class Order { public Guid Id { get; set; } public decimal Total { get; set; } }

Business logic in services:

public decimal CalculateTotal(Order order) { return order.Items.Sum(i => i.Price); }

Rich Domain Model (Recommended in DDD)

Business logic lives inside domain entities.

Example:

public class Order { private readonly List<OrderItem> _items = new(); public void AddItem(Guid productId, int qty, decimal price) { if (qty <= 0) throw new Exception("Qty must be > 0"); _items.Add(new OrderItem(productId, qty, price)); } public decimal GetTotal() => _items.Sum(i => i.Price * i.Quantity); }

Rich model:

  • Enforces business rules
  • Reduces duplication
  • Maintains invariants


๐ŸŽฏ Summary Table

ConceptOne-line definition
RepositoryAbstraction over data access for aggregates
Domain EventsThings that happened in the domain
Ubiquitous LanguageSame terms used across business & code
Bounded ContextClear boundary where a domain model is consistent
DTOData-only object for transferring data
AggregateCluster of domain objects with rules
Value ObjectImmutable concept compared by value
Rich ModelEntities with behavior (preferred in DDD)
Anemic ModelEntities with only properties (anti-pattern)




Monday, November 17, 2025

Architecture - Unit of Work (UOW) Pattern

1️⃣ What is Unit of Work?

Unit of Work (UoW) is a design pattern that:

  • Tracks changes made during a business transaction
  • Ensures all operations either succeed together (Commit)
  • Or fail together (Rollback)
  • Avoids partial writes to the database
  • Works like a transaction manager for repositories

๐Ÿ“Œ Simple Definition

UoW ensures that multiple database operations inside a business use case are treated as one atomic unit — either fully saved or fully reverted.


 


2️⃣ Why Do We Need Unit of Work?

Problems Without UoW

  • Calling multiple repositories → each saves independently
  • Leads to partial updates, inconsistent data, and race conditions
  • No coordination between repositories

With UoW

  • All repository actions are batched
  • Saved only when Commit() is called
  • If error occurs → Rollback() ensures consistency

3️⃣ How UoW Works Internally

EF Core already implements UoW concepts:

  • DbContext tracks changes (UoW)
  • DbContext.SaveChanges() = Commit

But in DDD or layered architecture, we wrap DbContext inside a custom UoW interface.


4️⃣ Commit & Rollback Explained

๐Ÿ‘‰ Commit

  • Saves all tracked changes to database
  • Calls SaveChanges() or SaveChangesAsync()
  • Ensures all operations succeed

๐Ÿ‘‰ Rollback

  • Cancels the transaction
  • Discards all uncommitted changes
  • Database remains unchanged

5️⃣ Unit of Work Flow (Simple)

Start UoW ↓ Use Repositories ↓ Make Changes (Add/Update/Delete) ↓ Commit() → All changes are saved OR Rollback() → All changes discarded

6️⃣ UoW with EF Core — Code Example

IUnitOfWork.cs

public interface IUnitOfWork : IDisposable { IProductRepository Products { get; } IOrderRepository Orders { get; } Task<int> CommitAsync(); // Commit = Save void Rollback(); // Rollback = Discard changes }

UnitOfWork.cs

public class UnitOfWork : IUnitOfWork { private readonly AppDbContext _context; public UnitOfWork(AppDbContext context) { _context = context; Products = new ProductRepository(context); Orders = new OrderRepository(context); } public IProductRepository Products { get; private set; } public IOrderRepository Orders { get; private set; } public async Task<int> CommitAsync() { return await _context.SaveChangesAsync(); } public void Rollback() { // Discard changes tracked by DbContext foreach (var entry in _context.ChangeTracker.Entries()) { switch (entry.State) { case EntityState.Modified: case EntityState.Deleted: case EntityState.Added: entry.State = EntityState.Detached; break; } } } public void Dispose() { _context.Dispose(); } }

7️⃣ Using Unit of Work — Example

Example Business Use Case: PlaceOrder

public async Task PlaceOrder(Order order) { using var uow = _unitOfWork; try { uow.Orders.Add(order); uow.Products.UpdateStock(order.ProductId, -order.Quantity); await uow.CommitAsync(); // Commit all } catch (Exception) { uow.Rollback(); // Discard changes throw; } }

✔ Both repositories share the same DbContext

✔ UoW ensures either:

  • Order saved + Stock updated → together
  • Or nothing saved → Rollback

8️⃣ Interview-Friendly Summary

Repository

  • Provides an abstraction over data logic
  • Works per entity (ProductRepository, OrderRepository)

Generic Repository

  • Provides base CRUD logic for all entities
  • Reduces duplication

Unit of Work

  • Controls multiple repositories
  • One transaction for the whole business operation
  • Offers Commit() and Rollback()
  • Maintains consistency

Architecture - Repository & Generic Repository Patterns

 ✅ 1. Repository Pattern

What it is

A Repository acts as an abstraction layer between your domain/business logic and the data access layer.
Instead of writing EF Core queries directly in controllers/services, you wrap them inside repository classes.

Why we use it

  • Keeps business logic independent from persistence logic

  • Easy to mock for unit testing

  • Central place to maintain DB queries


Example: Product Repository (Non-Generic)

Entity

public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } }

IProductRepository

public interface IProductRepository { Task<Product> GetByIdAsync(int id); Task<IEnumerable<Product>> GetAllAsync(); Task AddAsync(Product product); void Update(Product product); void Delete(Product product); }

Implementation

public class ProductRepository : IProductRepository { private readonly AppDbContext _context; public ProductRepository(AppDbContext context) { _context = context; } public async Task<Product> GetByIdAsync(int id) => await _context.Products.FindAsync(id); public async Task<IEnumerable<Product>> GetAllAsync() => await _context.Products.ToListAsync(); public async Task AddAsync(Product product) => await _context.Products.AddAsync(product); public void Update(Product product) => _context.Products.Update(product); public void Delete(Product product) => _context.Products.Remove(product); }

๐Ÿ‘‰ This is a repository dedicated only to Product.


2. Generic Repository Pattern

What it is

A Generic Repository provides a reusable base class for CRUD operations.
Instead of writing a repository per entity, you write one that works with all entities.

Why we use it

  • Avoids duplicated CRUD code

  • Cleaner structure

  • Extensible for entity-specific repositories


IGenericRepository

public interface IGenericRepository<T> where T : class { Task<T> GetByIdAsync(int id); Task<IEnumerable<T>> GetAllAsync(); Task AddAsync(T entity); void Update(T entity); void Delete(T entity); }

Implementation

public class GenericRepository<T> : IGenericRepository<T> where T : class { protected readonly AppDbContext _context; public GenericRepository(AppDbContext context) { _context = context; } public async Task<T> GetByIdAsync(int id) => await _context.Set<T>().FindAsync(id); public async Task<IEnumerable<T>> GetAllAsync>() => await _context.Set<T>().ToListAsync(); public async Task AddAsync(T entity) => await _context.Set<T>().AddAsync(entity); public void Update(T entity) => _context.Set<T>().Update(entity); public void Delete(T entity) => _context.Set<T>().Remove(entity); }

๐Ÿ”ฅ Entity-Specific Repository + Generic Repository

Sometimes we extend generic repository for custom queries:

public interface IProductRepository : IGenericRepository<Product> { Task<IEnumerable<Product>> GetProductsAbovePrice(decimal price); }
public class ProductRepository : GenericRepository<Product>, IProductRepository { public ProductRepository(AppDbContext context) : base(context) { } public async Task<IEnumerable<Product>> GetProductsAbovePrice(decimal price) { return await _context.Products .Where(p => p.Price > price) .ToListAsync(); } }

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


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...