Skip to main content

Microservice Architecture with CQRS using Product and Order APIs


In a microservice architecture, applications are divided into smaller, independent services that communicate with each other. The CQRS (Command Query Responsibility Segregation) pattern further improves this architecture by separating the read and write operations for better scalability and performance. In this blog, we will explore how to implement a microservice architecture using CQRS with Product and Order services as examples.

1. Understanding Microservice Architecture

Microservices decompose a large application into independent, self-contained services. Each service performs a specific business function, such as managing products or handling orders, and communicates with other services over lightweight protocols such as HTTP or messaging systems like Kafka. Key benefits include:

  • Scalability: Individual services can be scaled based on their own resource demands.
  • Resilience: Failure in one service doesn't bring down the entire system.
  • Autonomy: Each service can be developed and deployed independently.

2. Introducing CQRS Pattern

CQRS stands for Command Query Responsibility Segregation, a design pattern that separates read (query) and write (command) operations into different models. The key reasons for using CQRS in microservices:

  • Separation of Concerns: The write model (commands) focuses on business logic and data consistency, while the read model (queries) focuses on retrieving data efficiently.
  • Performance: The query model can be optimized for reading data (e.g., using caching or denormalized databases), while the command model can ensure strong consistency in write operations.
  • Scalability: Read and write models can be scaled independently based on the specific load requirements.

In our case, we will apply CQRS to the Product and Order microservices to handle reads and writes more effectively.

3. Product and Order APIs with CQRS

3.1 Product API
  • Responsibilities: The Product API manages information about products such as creating, updating, and retrieving product details.

  • Commands (Write Operations):

    • CreateProduct
    • UpdateProduct
    • DeleteProduct
  • Queries (Read Operations):

    • GetProductById
    • GetAllProducts
3.2 Order API
  • Responsibilities: The Order API handles customer orders, including creating new orders, updating order statuses, and retrieving order history.

  • Commands:

    • CreateOrder
    • UpdateOrderStatus
    • CancelOrder
  • Queries:

    • GetOrderById
    • GetOrdersByCustomer

4. Implementing CQRS in Product and Order APIs

4.1 Command Model Implementation

In the CQRS pattern, the command model focuses on handling state-changing operations. For example, in the Product service, creating or updating a product involves changing the state of the system.

Example for the Product Service:

public class CreateProductCommand : IRequest<Guid>
{
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
}

public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, Guid>
{
    private readonly IProductRepository _productRepository;

    public CreateProductCommandHandler(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }

    public async Task<Guid> Handle(CreateProductCommand request, CancellationToken cancellationToken)
    {
        var product = new Product
        {
            Id = Guid.NewGuid(),
            Name = request.Name,
            Description = request.Description,
            Price = request.Price
        };

        await _productRepository.AddAsync(product);
        return product.Id;
    }
}

In this example, the CreateProductCommand defines the data required to create a product, and the CreateProductCommandHandler handles the logic for processing that command, such as saving the product to the database.

4.2 Query Model Implementation

The query model is optimized for reading data. It might use a separate database or a denormalized structure to speed up data retrieval, depending on the requirements.

Example for the Order Service:

public class GetOrderByIdQuery : IRequest<OrderDto>
{
    public Guid OrderId { get; set; }
}

public class GetOrderByIdQueryHandler : IRequestHandler<GetOrderByIdQuery, OrderDto>
{
    private readonly IOrderReadRepository _orderReadRepository;

    public GetOrderByIdQueryHandler(IOrderReadRepository orderReadRepository)
    {
        _orderReadRepository = orderReadRepository;
    }

    public async Task<OrderDto> Handle(GetOrderByIdQuery request, CancellationToken cancellationToken)
    {
        var order = await _orderReadRepository.GetByIdAsync(request.OrderId);
        return new OrderDto
        {
            Id = order.Id,
            CustomerId = order.CustomerId,
            Items = order.Items,
            TotalAmount = order.TotalAmount
        };
    }
}

The GetOrderByIdQuery fetches an order by its ID, while the GetOrderByIdQueryHandler is responsible for retrieving the data from a read-optimized repository (e.g., a read database).

5. Event Sourcing

In a CQRS-based system, event sourcing can be used for write operations. Instead of storing the current state, the system stores a series of events that describe the changes made to the state. This approach ensures strong consistency and auditability.

Example of an event in the Order Service:

public class OrderCreatedEvent
{
    public Guid OrderId { get; }
    public DateTime CreatedAt { get; }
    public List<OrderItem> Items { get; }

    public OrderCreatedEvent(Guid orderId, List<OrderItem> items)
    {
        OrderId = orderId;
        Items = items;
        CreatedAt = DateTime.UtcNow;
    }
}

When an order is created, an OrderCreatedEvent is emitted. These events are then stored, allowing the system to reconstruct the state of the order by replaying events.

6. Benefits of CQRS in Microservices

  • Separation of Concerns: With CQRS, we separate the logic for handling commands (state changes) from queries (data retrieval), making it easier to scale and maintain each aspect of the system independently.

  • Performance Optimization: By having a dedicated query model, we can use techniques like caching, denormalized databases, or read replicas to optimize the performance of read-heavy scenarios, which is especially beneficial in services like the Order Service where read operations might outnumber writes.

  • Independent Scaling: Since the command and query models are separate, you can scale them independently based on usage patterns. For example, if the Product Service sees more reads than writes, you can scale the query side without affecting the write side.

  • Event-Driven Architecture: Event sourcing allows you to build an event-driven architecture where microservices can react to changes in other services. For example, when an order is placed, the Order Service can emit an event that the Product Service consumes to update product availability.

7. Challenges and Considerations

  • Consistency: Since the read and write models can use different databases, there might be a delay between the time when data is written and when it becomes available for querying. This is known as eventual consistency.

  • Complexity: Implementing CQRS introduces additional complexity due to the separation of read and write models, and potentially the need for event sourcing and messaging systems.

  • Data Duplication: In cases where denormalization is used in the query model, there may be some data duplication, which can increase storage costs.

Conclusion

By using the CQRS pattern with Product and Order services in a microservice architecture, we can achieve better scalability, maintainability, and performance. Separating commands and queries allows each microservice to handle its read and write operations more efficiently, making the system more resilient and capable of handling complex business requirements.

The combination of CQRS and microservices is particularly powerful in scenarios where different services have varying loads for read and write operations, allowing for optimized resource management and service independence.

Comments

Popular posts from this blog

Implementing and Integrating RabbitMQ in .NET Core Application: Shopping Cart and Order API

RabbitMQ is a robust message broker that enables communication between services in a decoupled, reliable manner. In this guide, we’ll implement RabbitMQ in a .NET Core application to connect two microservices: Shopping Cart API (Producer) and Order API (Consumer). 1. Prerequisites Install RabbitMQ locally or on a server. Default Management UI: http://localhost:15672 Default Credentials: guest/guest Install the RabbitMQ.Client package for .NET: dotnet add package RabbitMQ.Client 2. Architecture Overview Shopping Cart API (Producer): Sends a message when a user places an order. RabbitMQ : Acts as the broker to hold the message. Order API (Consumer): Receives the message and processes the order. 3. RabbitMQ Producer: Shopping Cart API Step 1: Install RabbitMQ.Client Ensure the RabbitMQ client library is installed: dotnet add package RabbitMQ.Client Step 2: Create the Producer Service Add a RabbitMQProducer class to send messages. RabbitMQProducer.cs : using RabbitMQ.Client; usin...

How Does My .NET Core Application Build Once and Run Everywhere?

One of the most powerful features of .NET Core is its cross-platform nature. Unlike the traditional .NET Framework, which was limited to Windows, .NET Core allows you to build your application once and run it on Windows , Linux , or macOS . This makes it an excellent choice for modern, scalable, and portable applications. In this blog, we’ll explore how .NET Core achieves this, the underlying architecture, and how you can leverage it to make your applications truly cross-platform. Key Features of .NET Core for Cross-Platform Development Platform Independence : .NET Core Runtime is available for multiple platforms (Windows, Linux, macOS). Applications can run seamlessly without platform-specific adjustments. Build Once, Run Anywhere : Compile your code once and deploy it on any OS with minimal effort. Self-Contained Deployment : .NET Core apps can include the runtime in the deployment package, making them independent of the host system's installed runtime. Standardized Libraries ...

Clean Architecture: What It Is and How It Differs from Microservices

In the tech world, buzzwords like   Clean Architecture   and   Microservices   often dominate discussions about building scalable, maintainable applications. But what exactly is Clean Architecture? How does it compare to Microservices? And most importantly, is it more efficient? Let’s break it all down, from understanding the core principles of Clean Architecture to comparing it with Microservices. By the end of this blog, you’ll know when to use each and why Clean Architecture might just be the silent hero your projects need. What is Clean Architecture? Clean Architecture  is a design paradigm introduced by Robert C. Martin (Uncle Bob) in his book  Clean Architecture: A Craftsman’s Guide to Software Structure and Design . It’s an evolution of layered architecture, focusing on organizing code in a way that makes it  flexible ,  testable , and  easy to maintain . Core Principles of Clean Architecture Dependency Inversion : High-level modules s...