Skip to content

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.

Commands are simple objects that typically carry the data required to perform an action. They extend the Command class from the library, with the type of the command’s result as its type argument (see Returning a value). A command that returns nothing extends Command without a type argument:

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

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

The result type exists for the compiler only: Command adds no property to the command, so its payload stays the same. Extending it is optional. Any class works as a command, such as a class that implements the ICommand interface, as in 3.x, but the result of executing it is then typed any.

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<CreditAccountCommand> {
constructor(private readonly accountRepository: AccountRepository) {}
async execute(command: CreditAccountCommand): Promise<void> {
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.

The execute() method of a command handler returns a promise, and whatever it resolves to is handed back to the code that executed the command. Commands usually return the identifier of what they created, so the caller knows where to find it. The type argument of Command is the type of that result. ICommandHandler takes it from the command, and @CommandHandler() only accepts a handler whose execute() resolves to it:

import { Command, CommandHandler, type ICommandHandler } from '@ocoda/event-sourcing';
export class OpenAccountCommand extends Command<string> {
constructor(public readonly accountOwnerIds?: string[]) {
super();
}
}
@CommandHandler(OpenAccountCommand)
export class OpenAccountCommandHandler implements ICommandHandler<OpenAccountCommand> {
constructor(private readonly accountRepository: AccountRepository) {}
async execute(command: OpenAccountCommand): Promise<string> {
const accountId = AccountId.generate();
const account = Account.open(accountId, command.accountOwnerIds?.map(AccountOwnerId.from));
await this.accountRepository.save(account);
return accountId.value;
}
}

For a command class that doesn’t extend Command, the second type parameter of ICommandHandler describes the result, as in 3.x: ICommandHandler<OpenAccountCommand, string>.

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: [OpenAccountCommandHandler, CreditAccountCommandHandler, AccountRepository],
})
export class AccountModule {}

The handlers are discovered when the application bootstraps, no matter in which module they are provided, see Module configuration. A handler may be request-scoped: the bus then resolves it for every call, in the context of the request you pass as commandBus.execute(command, { request }), see request-scoped handlers.

Commands are executed through the CommandBus. It’s available for injection in any provider of your application, see Module configuration. The bus looks up the handler that is registered for the class of the command and resolves to the result of that handler.

import { Body, Controller, Param, Post } from '@nestjs/common';
import { CommandBus } from '@ocoda/event-sourcing';
@Controller('accounts')
export class AccountController {
constructor(private readonly commandBus: CommandBus) {}
@Post()
async open(@Body('accountOwnerIds') accountOwnerIds?: string[]): Promise<{ accountId: string }> {
// OpenAccountCommand extends Command<string>, so accountId is a string
const accountId = await this.commandBus.execute(new OpenAccountCommand(accountOwnerIds));
return { accountId };
}
@Post(':accountId/credit')
async credit(@Param('accountId') accountId: string, @Body('amount') amount: number): Promise<void> {
await this.commandBus.execute(new CreditAccountCommand(accountId, amount));
}
}

For a command class that doesn’t extend Command, execute() resolves to any. Pass the command and the result type as type arguments to type it, as in 3.x: this.commandBus.execute<OpenAccountCommand, string>(command).

The handlers are registered by class. A subclass of a command reaches the handler of its parent, unless it has a handler of its own. If there’s no handler registered for the class of a command or any of its parent classes, the promise that execute() returns rejects with a CommandHandlerNotFoundException.

Guidelines for adding Commands & Command Handlers

Section titled “Guidelines for adding Commands & Command Handlers”
  1. 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.

  2. 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.

  3. Isolate business logic in the Command Handler

    Section titled “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.
  4. 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, closing an account that is already closed doesn’t have to fail, the handler can simply stop.

    @CommandHandler(CloseAccountCommand)
    export class CloseAccountCommandHandler implements ICommandHandler<CloseAccountCommand, void> {
    constructor(private readonly accountRepository: AccountRepository) {}
    async execute({ accountId }: CloseAccountCommand): Promise<void> {
    const account = await this.accountRepository.getById(AccountId.from(accountId));
    if (account.closedOn) {
    return; // Already closed, executing the command again has no further effect
    }
    account.close();
    await this.accountRepository.save(account);
    }
    }
  5. Command Handlers Should Focus on One Aggregate

    Section titled “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.

  6. Commands don’t return data beyond identifiers

    Section titled “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.