/ Docs

Commands

What is a Command?

A command expresses an intent to change state. It carries the data needed to validate and perform a domain action, but it does not guarantee the action will succeed. Commands are validated, passed to a handler, and — if business rules are satisfied — produce one or more events.

Commands in Bounda

Commands live inside domain/<aggregate>/commands/ and are defined as individual TypeScript files. Each command file exports:

  • payload — a function that receives Zod’s z and returns a validation schema for the command data.
  • handler — an async function that contains the business logic and returns event(s).
domain/
  order/
    commands/
      place-order.ts
      cancel-order.ts

Defining a Command

import type { Command } from "./+types/place-order";

export const payload: Command.PayloadFunction = ({ z }) =>
  z.object({
    customerId: z.string(),
    items: z.array(z.object({
      productId: z.string(),
      quantity: z.number().positive(),
      price: z.number().positive(),
    })),
  });

export async function handler({ command, events }: Command.HandlerArgs) {
  const { customerId, items } = command.payload;
  const total = items.reduce((sum, item) => sum + item.quantity * item.price, 0);

  return events.orderPlaced({
    aggregateId: command.aggregateId,
    customerId,
    items,
    total,
  });
}

The payload function defines what data the command accepts. Bounda validates incoming data against this schema before the handler runs. If validation fails, the handler is never called.

Handler Arguments

The handler function receives an object with:

ArgumentDescription
commandThe validated command with aggregateId, payload, and metadata
eventsTyped event builders for the aggregate (e.g., events.orderPlaced(...))
stateResolverAsync function that returns the current aggregate state

Using State in Handlers

When a command needs to enforce rules based on existing state, use stateResolver():

export async function handler({ command, events, stateResolver }: Command.HandlerArgs) {
  const state = await stateResolver();

  if (state.status === "cancelled") {
    throw new Error("Cannot confirm a cancelled order");
  }

  return events.orderConfirmed({ aggregateId: command.aggregateId });
}

Dispatching Commands

After bootstrapping the application, commands are available on the app.commands object. The method name is the camelCase form of the command file name:

await app.commands.placeOrder({
  aggregateId: "order-123",
  customerId: "cust-456",
  items: [
    { productId: "prod-1", quantity: 2, price: 29.99 },
  ],
});

Commands with Collaborators

When a command needs external dependencies (e.g., an email sender, a payment gateway), use a folder structure instead of a single file:

domain/
  order/
    commands/
      send-confirmation/
        index.ts
        notification-sender.console.ts
        notification-sender.sendgrid.ts

The index.ts contains the payload and handler exports. The collaborator files provide swappable implementations. See Collaborators for details.

Returning Multiple Events

A handler can return an array of events when a single command produces multiple state changes:

export async function handler({ command, events }: Command.HandlerArgs) {
  return [
    events.orderPlaced({ aggregateId: command.aggregateId, ...command.payload }),
    events.inventoryReserved({ aggregateId: command.aggregateId, items: command.payload.items }),
  ];
}
  • Events — the immutable facts produced by command handlers
  • Collaborators — injecting external dependencies into commands
  • Scheduled Commands — deferring command execution with a delay