NestJS CQRS Pattern & Microservice Architecture
Separating read/write concerns with Commands, Queries, and Events in NestJS — enabling independently scalable, testable microservices connected via a message broker.
Overview
CQRS (Command Query Responsibility Segregation) splits your application model into two distinct paths: Commands mutate state, Queries read it. Combined with NestJS's first-class CQRS module and a message broker like RabbitMQ, you get a foundation for independently scalable microservices.
Project Structure
src/
├── orders/
│ ├── commands/
│ │ ├── create-order.command.ts
│ │ └── create-order.handler.ts
│ ├── queries/
│ │ ├── get-orders.query.ts
│ │ └── get-orders.handler.ts
│ ├── events/
│ │ ├── order-created.event.ts
│ │ └── order-created.handler.ts
│ ├── orders.controller.ts
│ └── orders.module.ts
Installing @nestjs/cqrs
npm install @nestjs/cqrs
Defining a Command
// commands/create-order.command.ts
export class CreateOrderCommand {
constructor(
public readonly userId: string,
public readonly items: Array<{ productId: string; qty: number }>,
) {}
}
// commands/create-order.handler.ts
import { CommandHandler, ICommandHandler, EventBus } from "@nestjs/cqrs";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { CreateOrderCommand } from "./create-order.command";
import { Order } from "../entities/order.entity";
import { OrderCreatedEvent } from "../events/order-created.event";
@CommandHandler(CreateOrderCommand)
export class CreateOrderHandler implements ICommandHandler<CreateOrderCommand> {
constructor(
@InjectRepository(Order) private readonly repo: Repository<Order>,
private readonly eventBus: EventBus,
) {}
async execute(command: CreateOrderCommand): Promise<Order> {
const order = this.repo.create({
userId: command.userId,
items: command.items,
status: "pending",
});
await this.repo.save(order);
// Publish domain event — decoupled side effects
this.eventBus.publish(new OrderCreatedEvent(order.id, order.userId));
return order;
}
}
Defining a Query
// queries/get-orders.query.ts
export class GetOrdersQuery {
constructor(public readonly userId: string, public readonly page: number = 1) {}
}
// queries/get-orders.handler.ts
import { QueryHandler, IQueryHandler } from "@nestjs/cqrs";
import { GetOrdersQuery } from "./get-orders.query";
@QueryHandler(GetOrdersQuery)
export class GetOrdersHandler implements IQueryHandler<GetOrdersQuery> {
async execute(query: GetOrdersQuery) {
// Queries hit a READ replica or a denormalized read model — never the write DB
return readDb.orders
.find({ userId: query.userId })
.skip((query.page - 1) * 20)
.limit(20);
}
}
Domain Events & Side Effects
// events/order-created.handler.ts
import { EventsHandler, IEventHandler } from "@nestjs/cqrs";
import { OrderCreatedEvent } from "./order-created.event";
import { NotificationService } from "@/notifications/notification.service";
import { InventoryClient } from "@/inventory/inventory.client";
@EventsHandler(OrderCreatedEvent)
export class OrderCreatedHandler implements IEventHandler<OrderCreatedEvent> {
constructor(
private readonly notifications: NotificationService,
private readonly inventory: InventoryClient,
) {}
async handle(event: OrderCreatedEvent) {
// Decoupled — orders module doesn't know about notifications
await this.notifications.sendOrderConfirmation(event.userId, event.orderId);
await this.inventory.reserveStock(event.orderId);
}
}
Connecting via a Message Broker (RabbitMQ)
// main.ts — Microservice transport
import { NestFactory } from "@nestjs/core";
import { MicroserviceOptions, Transport } from "@nestjs/microservices";
const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
transport: Transport.RMQ,
options: {
urls: [process.env.RABBITMQ_URL!],
queue: "orders_queue",
queueOptions: { durable: true },
},
});
await app.listen();
Module Wiring
// orders.module.ts
import { Module } from "@nestjs/common";
import { CqrsModule } from "@nestjs/cqrs";
import { CreateOrderHandler } from "./commands/create-order.handler";
import { GetOrdersHandler } from "./queries/get-orders.handler";
import { OrderCreatedHandler } from "./events/order-created.handler";
const CommandHandlers = [CreateOrderHandler];
const QueryHandlers = [GetOrdersHandler];
const EventHandlers = [OrderCreatedHandler];
@Module({
imports: [CqrsModule],
providers: [...CommandHandlers, ...QueryHandlers, ...EventHandlers],
})
export class OrdersModule {}
Key Takeaways
- Commands mutate — they return the new aggregate, not a read model
- Queries never mutate — they can hit a read replica for zero contention
- Events decouple cross-domain side effects (email, inventory, analytics)
- Each handler is a small, focused class — trivially unit-testable
- Message broker (RabbitMQ/Kafka) makes services independently deployable