Ryan Kim

Menu

CQRS Pattern with NestJS

CQRS Pattern with NestJS

Oct 20, 2022

CQRS Pattern with NestJS

ref: https://www.vinsguru.com/cqrs-pattern/

CQRS(Command Query Responsibility Segregation) Pattern is on of the Microservice Design Patterns to independently scale read and write workloads of an application & h ave well optimized data schema.

Read vs Write Models

When we design applications, we create entity classes and corresponding repository classes for CRUD operations.

We use the same model classes for all the CRUD operations. However, these applications might have completely different READ and WRITE requirements.

For example, crating a new user or an order is simple and direct. But if we consider READ requirements, we would not simply want to get all the users or orders. Instead, we would be interested in getting all the orders details of a specific user which involves a lot of aggregated information that includes multipe tables.

To solve this problem, we can have separate models for READ and WRITE.

Read vs Write Traffic

Most of the web based applications heavily depend on read operation. To make it more efficient, we can have separate microservices for READ and WRITE so that they can be scaled in/out independently depending on the situation.

CQRS Pattern stands for Command Query Responsibility Segregation Pattern.

This pattern separates Command (write) and Query (read) models of an application to scale read and write operations independently.

  • Command: modifies the data and does not return anything (WRITE)
  • Query: does not modify but returns data (READ)

orders-service.png

Let’s assume that we have an interface for the order service for the read and write operations as shown below.

interface OrderService {
  placeOrder(userIndex: Number, productIndex: Number): Promise<void>;
  cancelOrder(orderId: Number): Promise<void>;
  getTotalSale(): Promise<Number>;
}
  • It has mujltiple responsibilities like placing the order, canceling the order and querying the table which produce different types of results.
  • Cancelling the order would reauire additional business logic like “order date should be within 30 days.”, etc.

CQRS Pattern - Read & Write Interfaces:

Instaed of having one single interface that is responsible for both READ and WRITE operations, let’s spplit it into two different interfaces.

  • Query Service: handles all the READ requirements
  • Command Service: handles all otehr requirements that modify the data.

cqrs-read-write-interface.png

OrderQueryService:

interface OrderQueryService {
  getSaleSummaryGroupByState(): Promise<PurchaseOrderSummaryDto[]>;
  getSaleSummaryByState(state: string): Promise<PurchaseOrderSummaryDto>;
  getTotalSale(): Promise<Number>
}

OrderCommandService:

interface OrderCommandService {
  createOrder(userIndex: Number, productIndex: Number): Promise<void>;
  cancelOrder(orderId: Number): Promise<void>;
}

Command vs Query - Controllers

We are going to implement dedicated controllers for Query(READ) and Command(WRITE).

cqrs-controller.png

OrderQueryController:

The methods of OrderQueryController handles only GET requests. The class perform tasks which do not modify the data.

@Controller('/orders')
export class OrderQueryController {

  constructor(private orderQueryService: OrderQueryService) {}
  
  @Get('/sumary')
  getSummary(): Promise<PurchaseOrderSummaryDto[]> {
    return this.orderQueryService.getSaleSummaryGroupByState();
  }

  @Get('/summary/:state')
  getStateSumary(@Param('state') state: string): Promise<PurchaseOrderSummaryDto> {
    return this.orderQueryService.getSaleSummaryByState(state);
  }

  @Get('/total-sale')
  getTotalSale(): Promise<Number> {
    return this.orderQueryService.getTotalSale();
  }
}

QueryCommendController:

@Controller("/orders")
export class OrderQueryController {
  constructor(private orderCommandService: OrderCommandService) {}

  @Post("/sale")
  placeOrder(@Body() dto: OrderCommandDto): Promise<void> {
    this.orderCommandService.createOrder(dto.userIndex, dto.productIndex);
    return;
  }

  @Post("/cancel-order/:orderId")
  cancelOrder(@Param("orderId") orderId: Number): Promise<void> {
    this.orderCommandService.cancelOrder(orderId);
    return;
  }
}

CQRS Pattern - Scaling

Depending on the number and type of requests, we can maintain multiple instances of READ node or WRITE node. For instance, we can have one instance of app that handles WRITE and have multiple instances of app that handles READ requests. They can be scaled in/out independently.

To this end, we can place apps behind a load balancer/proxy like nginx so that READ/WRITE requests can be forwarded to appropriate instances.

cqrs-scaling.png

Command vs Query DB

In the above example, READ and WRITE nodes used the same database. We can even go on level further by separating database into TWO: one for READ and the other for WRITE as shwon below. Any write operations will push the changes to the READ database through message queue such as Kafka.

cqrs-message-queue.png

Summary

With CQRS Pattern, we can have separate microservices for READ and WRITE so that they can be scaled in/out independently depending on the situation. Furthermore, we can use separate, different types of DB for command and query operations as each database has its own benefits.