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

.NET 10: Your Ultimate Guide to the Coolest New Features (with Real-World Goodies!)

 Hey .NET warriors! 🤓 Are you ready to explore the latest and greatest features that .NET 10 and C# 14 bring to the table? Whether you're a seasoned developer or just starting out, this guide will show you how .NET 10 makes your apps faster, safer, and more productive — with real-world examples to boot! So grab your coffee ☕️ and let’s dive into the awesome . 💪 1️⃣ JIT Compiler Superpowers — Lightning-Fast Apps .NET 10 is all about speed . The Just-In-Time (JIT) compiler has been turbocharged with: Stack Allocation for Small Arrays 🗂️ Think fewer heap allocations, less garbage collection, and blazing-fast performance . Better Code Layout 🔥 Hot code paths are now smarter, meaning faster method calls and fewer CPU cache misses. 💡 Why you care: Your APIs, desktop apps, and services now respond quicker — giving users a snappy experience . 2️⃣ Say Hello to C# 14 — More Power in Your Syntax .NET 10 ships with C# 14 , and it’s packed with developer goodies: Field-Bac...