Command
WARNING
We created this page with the help of the GenAI tool.
We're currently double-checking it to ensure the information is 100% correct and free of hallucinations.
Commands represent the intention to perform a business operation. They are requests directed at a specific handler.
Overview
Commands differ from events in key ways:
| Aspect | Command | Event |
|---|---|---|
| Tense | Imperative (do this) | Past (this happened) |
| Outcome | May be rejected | Immutable fact |
| Naming | AddProductItem | ProductItemAdded |
| Multiplicity | Single handler | Multiple subscribers |
Commands express intent. The handler decides whether to accept or reject the request.
Type Definition
type Command<
CommandType extends string = string,
CommandData extends DefaultRecord = DefaultRecord,
CommandMetaData extends DefaultRecord | undefined = undefined,
> = Readonly<{
type: CommandType;
data: CommandData;
metadata?: CommandMetaData | DefaultCommandMetadata;
kind?: 'Command';
}>;
type DefaultCommandMetadata = { now: Date };| Property | Type | Description |
|---|---|---|
type | string | Unique command type name (e.g., 'AddProductItem') |
data | object | Request payload (must be a record, not primitive) |
metadata | object? | Infrastructure data (defaults to { now: Date }) |
kind | 'Command'? | Discriminator for union types with Events |
Basic Usage
Defining Command Types
import type { Command } from '@event-driven-io/emmett';
type AddProductItemToShoppingCart = Command<
'AddProductItemToShoppingCart',
{
shoppingCartId: string;
productItem: PricedProductItem;
}
>;Grouping commands into Union Types
Define all commands for a specific business process as a discriminated union:
import type { Command } from '@event-driven-io/emmett';
type ShoppingCartCommand =
| Command<
'OpenShoppingCart',
{
cartId: string;
clientId: string;
}
>
| Command<
'AddProductItem',
{
productId: string;
quantity: number;
}
>
| Command<
'RemoveProductItem',
{
productId: string;
quantity: number;
}
>
| Command<'ConfirmShoppingCart', {}>
| Command<'CancelShoppingCart', {}>;Creating Commands with Factory
You can either use a regular typescript setup or use the command factory function for runtime command creation:
import { command } from '@event-driven-io/emmett';
const addProduct = command<AddProductItem>('AddProductItem', {
productId: 'shoes-1',
quantity: 2,
});
// Result: { type: 'AddProductItem', data: {...}, kind: 'Command' }
// With timestamp metadata (default)
const addProductWithTime = command<AddProductItem>(
'AddProductItem',
{ productId: 'shoes-1', quantity: 2 },
{ now: new Date() },
);Commands with Custom Metadata
Metadata carries cross-cutting concerns:
type UserCommandMetadata = {
userId: string;
correlationId: string;
now: Date;
};
type AuthenticatedAddProductItem = Command<
'AddProductItem',
{ productId: string; quantity: number },
UserCommandMetadata
>;
const authenticatedCommand = command<AuthenticatedAddProductItem>(
'AddProductItem',
{ productId: 'shoes-1', quantity: 2 },
{ userId: 'user-123', correlationId: 'req-456', now: new Date() },
);Commands vs Events
Commands and events work together in the Decider pattern:
// Command: Request to add a product
type AddProductItem = Command<
'AddProductItem',
{
productId: string;
quantity: number;
}
>;
// Event: Result of successful command
type ProductItemAdded = Event<
'ProductItemAdded',
{
productId: string;
quantity: number;
price: number; // Enriched during handling
}
>;
// Decider decides command → events
const decide = (
command: AddProductItem,
state: ShoppingCart,
): ProductItemAdded[] => {
if (state.status !== 'Open') {
throw new IllegalStateError('Cart is not open');
}
return [
{
type: 'ProductItemAdded',
data: {
productId: command.data.productId,
quantity: command.data.quantity,
price: lookupPrice(command.data.productId),
},
},
];
};Utility Types
Extracting Command Properties
import type {
CommandTypeOf,
CommandDataOf,
CommandMetaDataOf,
} from '@event-driven-io/emmett';
type AddProductItem = Command<'AddProductItem', { productId: string }>;
type CmdType = CommandTypeOf<AddProductItem>; // 'AddProductItem'
type CmdData = CommandDataOf<AddProductItem>; // { productId: string }
type CmdMeta = CommandMetaDataOf<AddProductItem>; // undefinedAny Command
For generic handlers:
import type { AnyCommand } from '@event-driven-io/emmett';
function logCommand(command: AnyCommand): void {
console.log(`Command: ${command.type}`, command.data);
}Command Handling Patterns
Direct Handler
import { CommandHandler } from '@event-driven-io/emmett';
const handle = CommandHandler(eventStore, {
decide,
evolve,
initialState,
mapToStreamId: (command) => `shopping_cart-${command.data.cartId}`,
});
await handle({
type: 'AddProductItem',
data: { cartId: 'cart-123', productId: 'shoes-1', quantity: 2 },
});With Expected Version (Optimistic Concurrency)
await handle(
{
type: 'AddProductItem',
data: { cartId: 'cart-123', productId: 'shoes-1', quantity: 2 },
},
{ expectedStreamVersion: 5n },
);Best Practices
1. Use Imperative Names
Commands express intent:
// ✅ Good: Imperative
type AddProductItem = Command<'AddProductItem', {...}>;
type ConfirmOrder = Command<'ConfirmOrder', {...}>;
// ❌ Bad: Past tense (these are events)
type ProductItemAdded = Command<'ProductItemAdded', {...}>;2. Include Target Identity
Commands must identify their target:
// ✅ Good: Clear target
type AddProductItem = Command<
'AddProductItem',
{
cartId: string; // Target aggregate
productId: string;
quantity: number;
}
>;
// ❌ Bad: Missing target
type AddProductItem = Command<
'AddProductItem',
{
productId: string;
quantity: number;
// Which cart?
}
>;3. Keep Commands Focused
One intent per command:
// ✅ Good: Single responsibility
type AddProductItem = Command<'AddProductItem', {...}>;
type ApplyDiscount = Command<'ApplyDiscount', {...}>;
// ❌ Bad: Multiple operations
type AddProductItemAndApplyDiscount = Command<'AddProductItemAndApplyDiscount', {...}>;4. Validate at Boundaries
Validate command data before handling:
const handle = async (request: Request) => {
const data = await request.json();
// Validate at boundary
if (data.quantity <= 0) {
throw new ValidationError('Quantity must be positive');
}
// Create command with valid data
const cmd = command<AddProductItem>('AddProductItem', data);
await handler(cmd);
};Type Source
import type { DefaultRecord } from './';
export type Command<
CommandType extends string = string,
CommandData extends DefaultRecord = DefaultRecord,
CommandMetaData extends DefaultRecord | undefined = undefined,
> = Readonly<
CommandMetaData extends undefined
? {
type: CommandType;
data: Readonly<CommandData>;
metadata?: DefaultCommandMetadata | undefined;
}
: {
type: CommandType;
data: CommandData;
metadata: CommandMetaData;
}
> & { readonly kind?: 'Command' };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyCommand = Command<any, any, any>;
export type CommandTypeOf<T extends Command> = T['type'];
export type CommandDataOf<T extends Command> = T['data'];
export type CommandMetaDataOf<T extends Command> = T extends {
metadata: infer M;
}
? M
: undefined;
export type CreateCommandType<
CommandType extends string,
CommandData extends DefaultRecord,
CommandMetaData extends DefaultRecord | undefined = undefined,
> = Readonly<
CommandMetaData extends undefined
? {
type: CommandType;
data: CommandData;
metadata?: DefaultCommandMetadata | undefined;
}
: {
type: CommandType;
data: CommandData;
metadata: CommandMetaData;
}
> & { readonly kind?: 'Command' };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const command = <CommandType extends Command<string, any, any>>(
...args: CommandMetaDataOf<CommandType> extends undefined
? [
type: CommandTypeOf<CommandType>,
data: CommandDataOf<CommandType>,
metadata?: DefaultCommandMetadata | undefined,
]
: [
type: CommandTypeOf<CommandType>,
data: CommandDataOf<CommandType>,
metadata: CommandMetaDataOf<CommandType>,
]
): CommandType => {
const [type, data, metadata] = args;
return metadata !== undefined
? ({ type, data, metadata, kind: 'Command' } as CommandType)
: ({ type, data, kind: 'Command' } as CommandType);
};
export type DefaultCommandMetadata = { now: Date };See Also
- Getting Started - Commands
- Event - Facts produced by commands
- Command Handler - Processing commands
- Decider - Pattern combining commands and events
