Microservices Architecture is a design approach where an application is composed of small, loosely coupled, and independently deployable services. Each service is focused on a specific business capability and can be developed, deployed, and scaled independently.
Key Characteristics of Microservices:
- Single Responsibility: Each service handles a specific business function (e.g., Order Service, Payment Service).
- Decentralized Data Management: Each service manages its own database, ensuring data isolation.
- Independent Deployment: Services can be deployed and updated independently.
- Scalability: Individual services can be scaled based on demand.
- Technology Diversity: Different services can use different technologies and programming languages.
CQRS (Command Query Responsibility Segregation)
CQRS separates read (query) and write (command) operations into different models.
- Command: Handles operations that change the state (Create, Update, Delete).
- Query: Handles data retrieval without modifying the state.
Why Use CQRS?
- Separation of Concerns: Simplifies complex business logic by separating reads and writes.
- Performance Optimization: Queries and commands can be optimized differently (e.g., caching for reads).
- Scalability: Commands and queries can be scaled independently.
- Flexibility: Easier to apply Event Sourcing, logging, and auditing.
Why Microservices + CQRS Is Better Than Monolithic Architecture
Aspect | Monolithic Architecture | Microservices + CQRS |
---|---|---|
Deployment | All components are deployed together. | Services are deployed independently. |
Scalability | Entire application must scale. | Individual services scale as needed. |
Maintainability | Complex and tightly coupled code. | Easier to maintain with modular services. |
Data Management | Single database for the entire app. | Decentralized databases per service. |
Performance | Read/write operations share the same model. | Reads and writes are optimized separately. |
Technology Stack | Limited to one stack. | Services can use different technologies. |
Disadvantages:
- Complexity: CQRS can introduce complexity, especially when managing the two separate models and their synchronization.
- Increased Development Time: The overhead of maintaining two separate models, handling synchronization, and dealing with data consistency across the system can increase development time.
- Eventual Consistency: With separate command and query models, achieving eventual consistency may require additional mechanisms, such as events, which can lead to latency.
- Overhead for Simple Systems: For simple applications, implementing CQRS may be overkill as it adds unnecessary complexity without providing significant benefits.
Detailed C# Implementation (Microservices + CQRS)
Let’s implement a basic Order Service using CQRS in a microservices setup.
1. Command: Create Order
// Commands/CreateOrderCommand.cs
using MediatR;
public record CreateOrderCommand(string ProductName, int Quantity, decimal Price) : IRequest<Guid>;
2. Command Handler: Handle Order Creation
// Handlers/CreateOrderCommandHandler.cs
using MediatR;
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>
{
private readonly OrderDbContext _context;
public CreateOrderCommandHandler(OrderDbContext context)
{
_context = context;
}
public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
var order = new Order
{
Id = Guid.NewGuid(),
ProductName = request.ProductName,
Quantity = request.Quantity,
Price = request.Price,
CreatedAt = DateTime.UtcNow
};
_context.Orders.Add(order);
await _context.SaveChangesAsync(cancellationToken);
return order.Id;
}
}
3. Query: Get Order By ID
// Queries/GetOrderByIdQuery.cs
using MediatR;
public record GetOrderByIdQuery(Guid OrderId) : IRequest<Order>;
4. Query Handler: Fetch Order
// Handlers/GetOrderByIdQueryHandler.cs
using MediatR;
using Microsoft.EntityFrameworkCore;
public class GetOrderByIdQueryHandler : IRequestHandler<GetOrderByIdQuery, Order>
{
private readonly OrderDbContext _context;
public GetOrderByIdQueryHandler(OrderDbContext context)
{
_context = context;
}
public async Task<Order> Handle(GetOrderByIdQuery request, CancellationToken cancellationToken)
{
return await _context.Orders.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken);
}
}
5. API Controller
// Controllers/OrdersController.cs
using MediatR;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IMediator _mediator;
public OrdersController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderCommand command)
{
var orderId = await _mediator.Send(command);
return Ok(orderId);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(Guid id)
{
var order = await _mediator.Send(new GetOrderByIdQuery(id));
return order == null ? NotFound() : Ok(order);
}
}