Introduction
Command Query Responsibility Segregation (CQRS) separates read and write operations into different models. This enables independent scaling, optimized data stores for each operation type, and cleaner code for complex domains.
Key Concepts
- Command: Writes/mutations that change state
- Query: Reads that return data without side effects
- Command Handler: Executes a specific command
- Query Handler: Executes a specific query
Real World Context
CQRS benefits:
- Scale reads and writes independently
- Optimize read models for specific views
- Audit trail via command history
- Event sourcing compatibility
Deep Dive
Setup
bashnpm install @nestjs/cqrs
typescriptimport { CqrsModule } from '@nestjs/cqrs'; @Module({ imports: [CqrsModule], providers: [...CommandHandlers, ...QueryHandlers], }) export class UsersModule {}
Command Definition
typescriptexport class CreateUserCommand { constructor( public readonly email: string, public readonly name: string, public readonly password: string, ) {} } export class UpdateUserCommand { constructor( public readonly userId: string, public readonly name: string, ) {} }
Command Handler
typescriptimport { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; @CommandHandler(CreateUserCommand) export class CreateUserHandler implements ICommandHandler<CreateUserCommand> { constructor( private usersRepository: UsersRepository, private eventBus: EventBus, ) {} async execute(command: CreateUserCommand): Promise<string> { const { email, name, password } = command; const hashedPassword = await bcrypt.hash(password, 10); const user = await this.usersRepository.create({ email, name, password: hashedPassword, }); // Publish domain event this.eventBus.publish(new UserCreatedEvent(user.id, user.email)); return user.id; } }
Query Definition and Handler
typescriptexport class GetUserByIdQuery { constructor(public readonly userId: string) {} } @QueryHandler(GetUserByIdQuery) export class GetUserByIdHandler implements IQueryHandler<GetUserByIdQuery> { constructor(private usersRepository: UsersRepository) {} async execute(query: GetUserByIdQuery): Promise<UserDto> { const user = await this.usersRepository.findById(query.userId); if (!user) throw new NotFoundException(); return new UserDto(user); } }
Controller Using CQRS
typescriptimport { CommandBus, QueryBus } from '@nestjs/cqrs'; @Controller('users') export class UsersController { constructor( private commandBus: CommandBus, private queryBus: QueryBus, ) {} @Post() async createUser(@Body() dto: CreateUserDto) { const userId = await this.commandBus.execute( new CreateUserCommand(dto.email, dto.name, dto.password), ); return { id: userId }; } @Get(':id') async getUser(@Param('id') id: string) { return this.queryBus.execute(new GetUserByIdQuery(id)); } }
Domain Events
typescriptexport class UserCreatedEvent { constructor( public readonly userId: string, public readonly email: string, ) {} } @EventsHandler(UserCreatedEvent) export class UserCreatedHandler implements IEventHandler<UserCreatedEvent> { constructor(private emailService: EmailService) {} handle(event: UserCreatedEvent) { this.emailService.sendWelcomeEmail(event.email); } }
Common Pitfalls
- Overcomplicating simple CRUD: CQRS adds complexity. Use for complex domains.
- Commands returning data: Commands should return IDs at most, not full objects.
- Ignoring eventual consistency: Read models may be stale.
Best Practices
- Use CQRS for complex domains with different read/write patterns
- Commands should be imperative (CreateUser, UpdateOrder)
- Queries should be declarative (GetUserById, FindActiveOrders)
- Combine with event sourcing for full audit trail
- Start simple, adopt CQRS when complexity warrants it
Summary
CQRS separates reads and writes into distinct models. Use @nestjs/cqrs for commands and queries. Commands mutate state, queries return data. Combine with domain events for reactive side effects.
Code Examples
typescript
// Command: mutates state
export class CreateUserCommand {
constructor(public readonly email: string, public readonly name: string) {}
}
@CommandHandler(CreateUserCommand)
export class CreateUserHandler implements ICommandHandler<CreateUserCommand> {
async execute(command: CreateUserCommand) {
return this.usersRepo.create(command);
}
}
// Controller dispatches via bus
@Post()
create(@Body() dto: CreateUserDto) {
return this.commandBus.execute(new CreateUserCommand(dto.email, dto.name));
}