Getting started
Commands

Commands & Command Handlers

A Command represents a user or system's intent to perform a specific action within your domain. Commands are part of the CQRS pattern, where they express actions that change the state of the system. Each command is handled by a CommandHandler, which contains the logic for executing the command, such as interacting with the domain's aggregates and repositories.

Commands should clearly indicate the intent and ideally refer to the aggregate they operate on. The name of a command should use imperative language to describe the action, such as OpenAccountCommand or CreditAccountCommand.

Creating a Command

Commands are simple objects that typically carry the data required to perform an action. They are implemented by extending the ICommand interface from the library:

import { ICommand } from '@ocoda/event-sourcing';
 
export class CreditAccountCommand implements ICommand {
	constructor(
		public readonly accountId: string,
		public readonly amount: number,
	) {}
}

In this example, the CreditAccountCommand represents the intent to credit an account with a specific amount.

Creating a Command Handler

A CommandHandler is responsible for executing the logic associated with a specific command. In a typical flow, a CommandHandler interacts with aggregates, invokes business logic, and persists the results.

import { CommandHandler, type ICommandHandler } from '@ocoda/event-sourcing';
 
@CommandHandler(CreditAccountCommand)
export class CreditAccountCommandHandler implements ICommandHandler {
	constructor(private readonly accountRepository: AccountRepository) {}
 
	async execute(command: CreditAccountCommand): Promise<boolean> {
		const accountId = AccountId.from(command.accountId);
		const account = await this.accountRepository.getById(accountId);
 
		account.credit(command.amount);
 
		await this.accountRepository.save(account);
	}
}

In this example, the CreditAccountCommandHandler handles the execution of the CreditAccountCommand. It retrieves the Account aggregate from the repository, applies the necessary business logic, and saves the updated account to the repository.

Registering the Command Handler

CommandHandlers must be registered as providers in your NestJS application’s module, so that the framework can discover and execute them. For example, in a NestJS module:

import { Module } from '@nestjs/common';
 
@Module({
    providers: [CreditAccountCommandHandler, AccountRepository],
})
export class AccountModule {}

Guidelines for adding Commands & Command Handlers

Commands represent actions, not queries

Commands should focus solely on actions that change the state of the system. Avoid putting query logic inside commands, queries are handled separately in the CQRS pattern. Commands should be named imperatively, like OpenAccountCommand or CreditAccountCommand, to reflect the change they trigger.

Keep Commands simple

Commands are simply data carriers. They should contain only the properties needed to perform the action, without any additional logic or behavior. For instance, the OpenAccountCommand contains only the information required to open an account.

Isolate business logic in the Command Handler

The command handler is where the domain logic is executed, not the command itself. CommandHandlers should:

  • Interact with the appropriate aggregate(s).
  • Perform any necessary validations.
  • Apply the business logic.
  • Persist the changes to the repository. For instance, in the OpenAccountCommandHandler, the logic for opening an account is encapsulated in the Account.open() method, and the repository is used to save the resulting aggregate.

Ensure Command Handlers are idempotent

CommandHandlers should be idempotent where possible, meaning that executing the same command multiple times should have the same effect. This ensures robustness in distributed systems or event-driven architectures where retries may happen. For example, you may check if an account already exists before attempting to create a new one.

async execute(command: OpenAccountCommand): Promise<string> {
  if (await this.accountRepository.exists(command.accountOwner)) {
    throw new Error('Account already exists');
  }
 
  const accountId = AccountId.generate();
  const account = Account.open(accountId, command.accountOwnerIds?.map(AccountOwnerId.from));
 
  await this.accountRepository.save(account);
 
  return accountId.value;
}

Command Handlers Should Focus on One Aggregate

Each CommandHandler should typically interact with a single aggregate. If your command handler needs to interact with multiple aggregates, consider splitting the command into multiple commands or revisiting your design to ensure each aggregate is responsible for its own behavior.

Commands don't return data beyond identifiers

The goal of a command is to make a change, not to return detailed information. Commands usually return the identifier of the entity they modified or created. In the example above, the OpenAccountCommandHandler returns the accountId as confirmation of the action.