Command
Commands are a central piece of the CQRS (Command Query Responsibility Segregation) architecture. They encapsulate the intent to perform an action and carry the necessary data for that action.
A command is a simple object with the name of an operation and the data required to perform that operation. They can be thought of as serializable method calls.
"Many think of Commands as being Serializable Method Calls."
-- Greg Young
Implementation
Command Class
Commands are implemented in the application layer to validate information and transport them to the domain layer. They contain ValueObjects to ensure that information is validated by business rules.
The command file is located at: Task/src/useCases/write/AddTask/application/AddTaskCommand.ts
import { TitleValueObject } from "shared/domain/valueObjects/TitleValueObject";
import { DescriptionValueObject } from "shared/domain/valueObjects/DescriptionValueObject";
import { AssigneeIdValueObject } from "shared/domain/valueObjects/AssigneeIdValueObject";
export class AddTaskCommand {
constructor(readonly payload: AddTaskCommandPayload) {
}
}
export interface AddTaskCommandPayload {
title: TitleValueObject;
description: DescriptionValueObject;
assigneeId: AssigneeIdValueObject;
}
Key Components
- ValueObjects: Encapsulate and validate the primitive data types.
- Payload: The command's payload contains the necessary data for the operation.
Command Handler Class
Commands are processed by their respective command handlers. The command handler is responsible for executing all application logic, such as connecting to different data sources.
The command handler file is located at: Task/src/useCases/write/AddTask/application/AddTaskCommandHandler.ts
import { OverwriteProtectionBody, NotFoundError, PreconditionFailedError, UuidGenerator } from "@codebricks/codebricks-framework";
import { TaskAggregate } from "shared/domain/aggregate/TaskAggregate";
import { AddTaskCommand } from "./AddTaskCommand";
import { TaskRepository } from "shared/infrastructure/persistence/aggregate/TaskRepository";
import { TaskPublisherTrigger } from "shared/infrastructure/publishing/TaskPublisherTrigger";
export class AddTaskCommandHandler {
constructor(readonly repository: TaskRepository = new TaskRepository(), readonly publisher: TaskPublisherTrigger = new TaskPublisherTrigger()) {
}
@OverwriteProtectionBody(false)
async handleAddTask(command: AddTaskCommand): Promise<TaskAggregate> {
const aggregate: TaskAggregate = new TaskAggregate(UuidGenerator.uuid());
aggregate.addTask({
title: command.payload.title,
description: command.payload.description,
assigneeId: command.payload.assigneeId,
});
await this.repository.save(aggregate);
await this.publisher.trigger();
return aggregate;
}
}
Key Responsibilities of Command Handler
- Load or Create Aggregate: Load the aggregate from the database or create a new aggregate with a new UUID.
- Invoke Command Method: Call the appropriate command method on the aggregate.
- Persist Aggregate: Save the aggregate and its recorded events to the repository.
- Trigger Publisher: Trigger the event publisher to publish the event.
Best Practices
- Validation: Ensure that command payloads are validated using value objects.
- Immutability: Commands should be immutable once created.
- Clear Naming: Use clear and descriptive names for commands to reflect their intent.