Misusing Command and Query Buses in Modular Monoliths: A NestJS Perspective
In recent years, architectural patterns like Command Bus and Query Bus have gained popularity, especially in TypeScript backends using NestJS and its @nestjs/cqrs package. While these patterns offer separation of concerns, they are often overused in places where a simple, clean application service or facade would be better. The Problem: Overengineering in a Modular Monolith Consider this common anti-pattern in NestJS: // update-profile.command.ts export class UpdateProfileCommand { constructor(public readonly userId: string, public readonly nickname: string) {} } // update-profile.handler.ts @CommandHandler(UpdateProfileCommand) export class UpdateProfileHandler implements ICommandHandler { constructor(private readonly userRepository: UserRepository) {} async execute(command: UpdateProfileCommand) { const user = await this.userRepository.findById(command.userId); user.updateNickname(command.nickname); await this.userRepository.save(user); } } // get-profile.query.ts export class GetProfileQuery { constructor(public readonly userId: string) {} } // get-profile.handler.ts @QueryHandler(GetProfileQuery) export class GetProfileHandler implements IQueryHandler { constructor(private readonly userRepository: UserRepository) {} async execute(query: GetProfileQuery) { const user = await this.userRepository.findById(query.userId); return { userId: user.id, nickname: user.nickname }; } } // user-profile.controller.ts await this.commandBus.execute(new UpdateProfileCommand(userId, nickname)); await this.queryBus.execute(new GetProfileQuery(userId)); That’s four classes and four files just to write 2 methods. Where is the value in that? Where is the gain? ...