Suggest an editImprove this articleRefine the answer for “What is CQRS pattern and how to implement it in NestJS?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**CQRS (Command Query Responsibility Segregation)** separates write operations (commands) from read operations (queries) so each side can use different models and databases. NestJS implements this via `@nestjs/cqrs` with `CommandBus`, `QueryBus`, and dedicated handler classes. ```typescript this.commandBus.execute(new CreateOrderCommand(userId, items)); // writes to DB this.queryBus.execute(new GetOrderQuery(orderId)); // reads from cache ``` **Key point:** commands emit domain events after writing; queries read from projections built from those events.Shown above the full answer for quick recall.Answer (EN)Image**CQRS (Command Query Responsibility Segregation)** splits an application into two separate paths: commands that mutate state, and queries that only read. NestJS ships `@nestjs/cqrs` to wire this up without building the routing layer from scratch. ## Theory ### TL;DR - Restaurant analogy: the kitchen (commands) changes what is on the plate, the dining area (queries) just fetches the menu. Both sides scale independently. - Instead of one `OrdersService` with `create()` and `findOne()`, you get a `CreateOrderHandler` writing to Postgres and a `GetOrderHandler` reading from Redis. - `CommandBus.execute()` triggers writes, `QueryBus.execute()` triggers reads. - NestJS matches commands to handlers at runtime via `Reflect.metadata` on the class reference. - Use it when reads outnumber writes 10x or more, or when each side needs a completely different data shape. ### Quick example ```typescript import { CqrsModule, CommandBus, QueryBus } from '@nestjs/cqrs'; export class CreateOrderCommand { constructor(public readonly userId: string, public readonly items: Item[]) {} } export class GetOrderQuery { constructor(public readonly orderId: string) {} } @Controller('orders') export class OrdersController { constructor(private commandBus: CommandBus, private queryBus: QueryBus) {} @Post() create(@Body() dto: CreateOrderDto) { // Write path: routed to CreateOrderHandler return this.commandBus.execute(new CreateOrderCommand(dto.userId, dto.items)); } @Get(':id') find(@Param('id') id: string) { // Read path: routed to GetOrderHandler return this.queryBus.execute(new GetOrderQuery(id)); } } ``` The controller only knows about buses, not handlers. The bus finds the right handler by matching the class passed to `@CommandHandler()` or `@QueryHandler()` at registration time. ### Key difference from traditional CRUD Standard NestJS code puts `create()` and `findOne()` in the same service, sharing one repository and one entity shape. CQRS forces a split: the command side validates business rules and writes to a normalized relational table, the query side reads from a denormalized cache or a dedicated read DB. That separation means you can spin up 100 query pods without touching the write path at all. ### How `@nestjs/cqrs` works internally `CqrsModule` registers `CommandBus` and `QueryBus` as singletons in the NestJS DI container. When you call `commandBus.execute(new CreateOrderCommand(...))`, the bus reads the constructor name via `Reflect.metadata`, scans providers for the handler decorated with `@CommandHandler(CreateOrderCommand)`, and calls its `execute()` method. By default everything runs in-memory and in-process. For microservices, you replace the default transport with a Redis or RabbitMQ transporter so commands cross service boundaries. ### When to use CQRS - Read traffic is 10x or more than writes. Classic example: e-commerce product listings vs. order creation. - Each side needs a different data shape. The write side enforces business invariants, the read side returns flat, denormalized views for fast UI rendering. - You are building event-driven architecture where commands emit domain events and queries rebuild state from projections. - You need independent scaling: one write pod handling transactions, many query pods serving cached responses. Skip it for prototypes, internal tools, and any app without serious load. The handler/bus/event boilerplate triples your file count with zero payoff on small traffic. ### Comparison table | Aspect | Monolith CRUD | CQRS | |---|---|---| | Models | One entity for reads and writes | Separate command DTOs and query views | | Database | One relational DB | Writes to Postgres, reads from Redis or Mongo | | Scaling | Read replicas only | 1 write pod, 100 query pods independently | | Complexity | Low | High: handlers, buses, eventual consistency | | When to use | Prototypes, low-traffic apps | Systems with heavy read/write imbalance | ### Common mistakes **Reusing one DTO for commands and queries:** ```typescript // Wrong: one class serving both sides class OrderDto { userId: string; items: Item[]; total: number; // queries display this, but the command doesn't compute it yet } // Right: separate shapes per side class CreateOrderCommand { userId: string; items: Item[]; } class OrderSummaryView { id: string; total: number; status: string; } ``` Commands need input validation fields. Queries need display fields. One shared class couples the two sides and eliminates the separation the pattern exists for. **Forgetting to publish events after a command:** ```typescript // Wrong: saves to DB but never notifies the read side async execute(command: CreateOrderCommand) { await this.repo.save(order); // missing: this.eventBus.publish(new OrderCreatedEvent(order.id)) } ``` Without events, query handlers reading from a Redis projection never know a write happened. I have seen teams spend hours on "phantom data" that turned out to be a missing `eventBus.publish()` call. **Using the in-memory bus in a distributed system:** ```typescript // Wrong for microservices: commands stay in-process @Module({ imports: [CqrsModule] }) // Right: configure a distributed transporter // Plug in Redis or RabbitMQ transporter for cross-service dispatch ``` The default `CqrsModule` does not cross process boundaries. Commands sent in one service will never reach handlers in another service. **Skipping validation inside command handlers:** ```typescript // Wrong: invalid input goes straight to the DB async execute(command: CreateOrderCommand) { await this.repo.save(command); } // Right: validate before touching the DB async execute(command: CreateOrderCommand) { if (!command.userId) throw new BadRequestException('userId required'); const order = new Order(command.userId, command.items); await this.repo.save(order); } ``` ### Real-world usage - MedusaJS: commands write to a Postgres orders table, queries read from an Elasticsearch products index. - EventStoreDB + NestJS: commands append events, queries build read models from projections. - Uber-like trip systems: trip creation as a command to Kafka, status queries from Cassandra projections. - Admin panels with complex workflows: separate sagas handle multi-step operations without blocking read handlers. ### Follow-up questions **Q:** What is the difference between CQS and CQRS? **A:** CQS (Command Query Separation) is a method-level rule: a function either changes state or returns data, not both at once. CQRS is architectural: separate models, handlers, and potentially separate databases for each side. **Q:** How do you handle eventual consistency between the command and query sides? **A:** Command handlers publish domain events after writing. Event handlers subscribe via `@EventsHandler()` and update read models in Redis or Mongo. The gap between write and projection update is usually milliseconds, but you need to design your UI to handle it explicitly. **Q:** When does CQRS hurt more than help? **A:** In apps with under 10k requests per day or teams new to domain-driven design. The boilerplate multiplies without any scaling payoff. Start with plain services and migrate only when read and write patterns clearly diverge. **Q:** How do you query across bounded contexts? **A:** Don't join across contexts. Use domain events to project data into a denormalized query store that belongs to the context making the request. Direct joins couple contexts and undo the architecture. **Q (senior):** In a sharded system, how do you route commands to the right shard? **A:** Put a shard key like `tenantId` or `userId` in the command metadata. The bus transporter uses consistent hashing to route. Kafka partitions work well here. Watch for hot shards if key distribution is uneven. ## Examples ### Basic setup: command handler and query handler ```typescript // npm install @nestjs/cqrs // Command: carries the intent to write export class CreateOrderCommand { constructor( public readonly userId: string, public readonly items: { productId: string; quantity: number }[], ) {} } // Command handler: validates, writes to DB, fires event @CommandHandler(CreateOrderCommand) export class CreateOrderHandler implements ICommandHandler<CreateOrderCommand> { constructor( private readonly repo: OrderRepository, private readonly eventBus: EventBus, ) {} async execute(command: CreateOrderCommand) { if (!command.userId) throw new BadRequestException('userId required'); const order = await this.repo.save({ userId: command.userId, items: command.items, status: 'pending', }); this.eventBus.publish(new OrderCreatedEvent(order.id, order.userId)); return order; } } // Query: carries the intent to read export class GetOrderByIdQuery { constructor(public readonly orderId: string) {} } // Query handler: reads from the read repository (Redis, read replica, etc.) @QueryHandler(GetOrderByIdQuery) export class GetOrderByIdHandler implements IQueryHandler<GetOrderByIdQuery> { constructor(private readonly readRepo: OrderReadRepository) {} async execute(query: GetOrderByIdQuery) { return this.readRepo.findById(query.orderId); } } ``` The command handler writes to the main DB and fires an event. The query handler reads from a separate repository. They share nothing except the event that bridges them. ### Intermediate: event handler updating the read model ```typescript export class OrderCreatedEvent { constructor( public readonly orderId: string, public readonly userId: string, ) {} } // Event handler: keeps Redis projection in sync after every write @EventsHandler(OrderCreatedEvent) export class OrderCreatedHandler implements IEventHandler<OrderCreatedEvent> { constructor( private readonly redisService: RedisService, private readonly repo: OrderReadRepository, ) {} async handle(event: OrderCreatedEvent) { const order = await this.repo.findById(event.orderId); const key = `user:${event.userId}:orders`; const existing = JSON.parse(await this.redisService.get(key) || '[]'); await this.redisService.set( key, JSON.stringify([...existing, { id: order.id, status: order.status }]), ); } } // Query handler reads from Redis instead of Postgres @QueryHandler(GetUserOrdersQuery) export class GetUserOrdersHandler implements IQueryHandler<GetUserOrdersQuery> { constructor(private readonly redisService: RedisService) {} async execute(query: GetUserOrdersQuery) { const data = await this.redisService.get(`user:${query.userId}:orders`); return data ? JSON.parse(data) : []; } } ``` The event is the bridge. Without it, `GetUserOrdersQuery` returns stale data even after a successful write. ### Advanced: wiring everything in the module ```typescript @Controller('orders') export class OrdersController { constructor( private readonly commandBus: CommandBus, private readonly queryBus: QueryBus, ) {} @Post() async create(@Body() dto: CreateOrderDto, @CurrentUser() user: User) { return this.commandBus.execute(new CreateOrderCommand(user.id, dto.items)); } @Get(':id') async findOne(@Param('id') id: string) { return this.queryBus.execute(new GetOrderByIdQuery(id)); } @Get('user/me') async myOrders(@CurrentUser() user: User) { return this.queryBus.execute(new GetUserOrdersQuery(user.id)); } } // Every handler must be in providers or the bus throws at runtime const CommandHandlers = [CreateOrderHandler, CancelOrderHandler]; const QueryHandlers = [GetOrderByIdHandler, GetUserOrdersHandler]; const EventHandlers = [OrderCreatedHandler, OrderCancelledHandler]; @Module({ imports: [CqrsModule], controllers: [OrdersController], providers: [ ...CommandHandlers, ...QueryHandlers, ...EventHandlers, OrderRepository, OrderReadRepository, ], }) export class OrdersModule {} ``` If a handler is missing from `providers`, the bus throws "no handler found for [ClassName]" at the first call. That is the most common runtime error when setting up `@nestjs/cqrs` for the first time.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.