# Command

> Wrap an action and its arguments in an object so you can queue, log, retry or undo it separately from the code that triggers it.

- kind: pattern
- category: behavioral
- aliases: Action, Transaction
- canonical: https://aipatterns.dev/design-patterns/command
- inspired-by: https://refactoring.guru/design-patterns/command

## Intent

Turn a request into an object that carries everything needed to perform it later: the receiver, the arguments, and any state needed to reverse it. The code that triggers the action (a button, a scheduler, an API handler) then only holds a command and calls `execute`, without knowing what the action does.

## Forces

- **Trigger now** vs **run later**: the user clicks now, but the work goes on a queue, waits for a retry, or runs in a batch.
- **Doing an action** vs **remembering it**: undo, audit trails and replay need a record of what happened, not just its effect.
- **Many triggers** vs **one behavior**: a menu item, a keyboard shortcut and an API call should all run the same operation.
- **Thin callers** vs **rich operations**: the invoker should not know the receiver's API, but the operation needs it.

## Therefore

**Package each operation as an object with an `execute` method (and `undo` when needed), and have callers store and run those objects instead of calling the receiver directly.**

The command holds a reference to the receiver and the arguments. The invoker keeps commands in a history, a queue or a log. For undo, a command records what it needs to reverse itself at `execute` time. In languages with closures, a command without undo is a function value; a pair of closures `{ run, undo }` covers many undo cases too. Use classes when commands need names, serialization, or inspection.

## Structure

```mermaid
classDiagram
  class StockLedger {
    -StockCommand[] history
    +run(cmd) void
    +undoLast() void
  }
  class StockCommand {
    <<interface>>
    +execute() void
    +undo() void
  }
  class Receive {
    -String sku
    -int qty
    +execute() void
    +undo() void
  }
  class Pick {
    -String sku
    -int qty
    +execute() void
    +undo() void
  }
  class CycleCount {
    -int before
    +execute() void
    +undo() void
  }
  class Inventory {
    +qty(sku) int
    +set(sku, n) void
  }
  StockLedger o-- StockCommand
  StockCommand <|.. Receive
  StockCommand <|.. Pick
  StockCommand <|.. CycleCount
  Receive --> Inventory
  Pick --> Inventory
  CycleCount --> Inventory
```

### Participants

- **Command** (`StockCommand`): the interface the invoker calls. Usually `execute`, plus `undo` and a label if the invoker needs them.
- **Concrete commands** (`Receive`, `Pick`, `CycleCount`): bind a receiver to arguments, and capture the state needed for undo when they run.
- **Receiver** (`Inventory`): the object that does the real work. It knows nothing about commands.
- **Invoker** (`StockLedger`): runs commands and keeps the history, queue or log. It never looks inside a command.
- **Client**: creates commands with their receiver and arguments and hands them to the invoker.

## Use it when

- You need multi-level undo or redo for user actions.
- Actions must be queued, scheduled, retried, or run on another worker, and you want them as data.
- You need an audit log of what was done, in order, that can be replayed to rebuild state.
- The same operation is triggered from several places (toolbar, shortcut, command palette, API) and should behave identically.
- You want to group several operations into one macro or transaction that succeeds or rolls back together.

## Do not use it when

- You only need to pass behavior to be called later, with no undo, queue, history or logging. Pass a closure or function reference (`onClick={() => save(doc)}`); a `SaveCommand` class adds nothing.
- The "queue" is a background job system. Put plain serializable messages (`{ type: "ship", orderId }`) on the queue and map types to handlers. Command objects holding live receivers can't be serialized across processes.
- Undo is the only goal and the state is small or immutable. Keep previous versions of the state, or use [Memento](https://aipatterns.dev/design-patterns/memento.md) snapshots, instead of writing an inverse for every operation.
- The operation is a single call from a single place. Call the receiver directly.
- The variation is *how* a result is computed, not *which* action runs. That is [Strategy](https://aipatterns.dev/design-patterns/strategy.md).

## Application grill

**Inspect first:** the actions that need queueing, retry or undo, and whether a job system already runs (BullMQ, pg-boss, Celery, Sidekiq, SQS, platform queues); how command arguments would be serialized, since a queued command outlives the code version that queued it.

**Missing pieces:** asynchronous execution needs a queue. Prefer what the platform already provides (Vercel Queues, Cloudflare Queues, SQS); on an existing Postgres, pg-boss or SKIP LOCKED polling adds no new service; Redis plus BullMQ is the step up when throughput needs it. An undo stack for a session is in-memory; persisted undo is a table with a retention policy.

**Ask before you build:**

- **Scale:** how many commands per second, and how large are their payloads (queues bill per message and cap payload size)?
- **Performance:** how soon must a command run - immediately, or is seconds-later acceptable?
- **Reliability:** on failure, what happens - retry, dead-letter - and is the handler safe to run twice (idempotent-consumer)?
- **Budget:** managed queue versus Postgres-backed polling - whose operational budget decides?
- **Implementation:** who owns argument schema stability across deploys, and how are in-flight commands migrated?

## Implementation steps

1. Define the command interface from what the invoker needs: `execute`, and `undo` or `label` only if they are used.
2. For each operation, create a command that takes the receiver and arguments in its constructor.
3. In `execute`, call the receiver. If the command supports undo, capture the prior state here, not in the constructor.
4. Make the invoker record a command only after `execute` succeeds, so a failed action never lands in the history.
5. Point every trigger (button, shortcut, API route) at the invoker instead of the receiver.
6. Test each command's `execute` followed by `undo` returns the receiver to its original state.

## Example

A warehouse tool records stock movements. Clerks receive stock, pick it for orders, and correct counts after a cycle count. Every action is logged, a failed pick is refused without touching stock, and the last action can be undone.

```ts title="stock.ts"
class Inventory {
  private stock = new Map<string, number>();
  qty(sku: string): number {
    return this.stock.get(sku) ?? 0;
  }
  set(sku: string, n: number): void {
    this.stock.set(sku, n);
  }
}

interface StockCommand {
  readonly label: string;
  execute(): void;
  undo(): void;
}

class Receive implements StockCommand {
  constructor(private inv: Inventory, private sku: string, private n: number) {}
  get label() { return `receive ${this.n} ${this.sku}`; }
  execute() { this.inv.set(this.sku, this.inv.qty(this.sku) + this.n); }
  undo() { this.inv.set(this.sku, this.inv.qty(this.sku) - this.n); }
}

class Pick implements StockCommand {
  constructor(private inv: Inventory, private sku: string, private n: number) {}
  get label() { return `pick ${this.n} ${this.sku}`; }
  execute() {
    const onHand = this.inv.qty(this.sku);
    if (onHand < this.n) throw new Error(`${this.label}: only ${onHand} on hand`);
    this.inv.set(this.sku, onHand - this.n);
  }
  undo() { this.inv.set(this.sku, this.inv.qty(this.sku) + this.n); }
}

// Not reversible by arithmetic, so it captures the old value when it runs.
class CycleCount implements StockCommand {
  private before = 0;
  constructor(private inv: Inventory, private sku: string, private counted: number) {}
  get label() { return `count ${this.sku} = ${this.counted}`; }
  execute() {
    this.before = this.inv.qty(this.sku);
    this.inv.set(this.sku, this.counted);
  }
  undo() { this.inv.set(this.sku, this.before); }
}

class StockLedger {
  private history: StockCommand[] = [];
  readonly log: string[] = [];

  run(cmd: StockCommand): void {
    cmd.execute(); // throws before anything is recorded
    this.history.push(cmd);
    this.log.push(`do   ${cmd.label}`);
  }

  undoLast(): void {
    const cmd = this.history.pop();
    if (!cmd) return;
    cmd.undo();
    this.log.push(`undo ${cmd.label}`);
  }
}

const inv = new Inventory();
const ledger = new StockLedger();
ledger.run(new Receive(inv, "SKU-7", 40));
ledger.run(new Pick(inv, "SKU-7", 12));
try {
  ledger.run(new Pick(inv, "SKU-7", 500));
} catch (e) {
  console.log("refused:", (e as Error).message);
}
ledger.run(new CycleCount(inv, "SKU-7", 25));
ledger.undoLast();
console.log(inv.qty("SKU-7")); // 28
console.log(ledger.log.join("\n"));
```

```python title="stock.py"
from typing import Protocol

class Inventory:
    def __init__(self) -> None:
        self._stock: dict[str, int] = {}

    def qty(self, sku: str) -> int:
        return self._stock.get(sku, 0)

    def set(self, sku: str, n: int) -> None:
        self._stock[sku] = n

class StockCommand(Protocol):
    label: str
    def execute(self) -> None: ...
    def undo(self) -> None: ...

class Receive:
    def __init__(self, inv: Inventory, sku: str, n: int) -> None:
        self.inv, self.sku, self.n = inv, sku, n
        self.label = f"receive {n} {sku}"

    def execute(self) -> None:
        self.inv.set(self.sku, self.inv.qty(self.sku) + self.n)

    def undo(self) -> None:
        self.inv.set(self.sku, self.inv.qty(self.sku) - self.n)

class Pick:
    def __init__(self, inv: Inventory, sku: str, n: int) -> None:
        self.inv, self.sku, self.n = inv, sku, n
        self.label = f"pick {n} {sku}"

    def execute(self) -> None:
        on_hand = self.inv.qty(self.sku)
        if on_hand < self.n:
            raise ValueError(f"{self.label}: only {on_hand} on hand")
        self.inv.set(self.sku, on_hand - self.n)

    def undo(self) -> None:
        self.inv.set(self.sku, self.inv.qty(self.sku) + self.n)

class CycleCount:
    def __init__(self, inv: Inventory, sku: str, counted: int) -> None:
        self.inv, self.sku, self.counted = inv, sku, counted
        self.label = f"count {sku} = {counted}"
        self._before = 0

    def execute(self) -> None:
        self._before = self.inv.qty(self.sku)  # captured at run time, not construction
        self.inv.set(self.sku, self.counted)

    def undo(self) -> None:
        self.inv.set(self.sku, self._before)

class StockLedger:
    def __init__(self) -> None:
        self._history: list[StockCommand] = []
        self.log: list[str] = []

    def run(self, cmd: StockCommand) -> None:
        cmd.execute()  # raises before anything is recorded
        self._history.append(cmd)
        self.log.append(f"do   {cmd.label}")

    def undo_last(self) -> None:
        if self._history:
            cmd = self._history.pop()
            cmd.undo()
            self.log.append(f"undo {cmd.label}")

if __name__ == "__main__":
    inv, ledger = Inventory(), StockLedger()
    ledger.run(Receive(inv, "SKU-7", 40))
    ledger.run(Pick(inv, "SKU-7", 12))
    try:
        ledger.run(Pick(inv, "SKU-7", 500))
    except ValueError as e:
        print("refused:", e)
    ledger.run(CycleCount(inv, "SKU-7", 25))
    ledger.undo_last()
    print(inv.qty("SKU-7"))  # 28
    print("\n".join(ledger.log))
```

```go title="stock.go"
package main

import "fmt"

type Inventory struct{ stock map[string]int }

func NewInventory() *Inventory             { return &Inventory{stock: map[string]int{}} }
func (i *Inventory) Qty(sku string) int    { return i.stock[sku] }
func (i *Inventory) Set(sku string, n int) { i.stock[sku] = n }

type StockCommand interface {
	Execute() error
	Undo()
	Label() string
}

type Receive struct {
	Inv *Inventory
	SKU string
	N   int
}

func (c *Receive) Execute() error { c.Inv.Set(c.SKU, c.Inv.Qty(c.SKU)+c.N); return nil }
func (c *Receive) Undo()          { c.Inv.Set(c.SKU, c.Inv.Qty(c.SKU)-c.N) }
func (c *Receive) Label() string  { return fmt.Sprintf("receive %d %s", c.N, c.SKU) }

type Pick Receive // same fields as Receive, its own methods

func (c *Pick) Execute() error {
	onHand := c.Inv.Qty(c.SKU)
	if onHand < c.N {
		return fmt.Errorf("%s: only %d on hand", c.Label(), onHand)
	}
	c.Inv.Set(c.SKU, onHand-c.N)
	return nil
}
func (c *Pick) Undo()         { c.Inv.Set(c.SKU, c.Inv.Qty(c.SKU)+c.N) }
func (c *Pick) Label() string { return fmt.Sprintf("pick %d %s", c.N, c.SKU) }

type CycleCount struct {
	Inv     *Inventory
	SKU     string
	Counted int
	before  int // captured by Execute
}

func (c *CycleCount) Execute() error {
	c.before = c.Inv.Qty(c.SKU)
	c.Inv.Set(c.SKU, c.Counted)
	return nil
}
func (c *CycleCount) Undo()         { c.Inv.Set(c.SKU, c.before) }
func (c *CycleCount) Label() string { return fmt.Sprintf("count %s = %d", c.SKU, c.Counted) }

type StockLedger struct {
	history []StockCommand
	Log     []string
}

func (l *StockLedger) Run(c StockCommand) error {
	if err := c.Execute(); err != nil {
		return err // failed commands never enter the history
	}
	l.history = append(l.history, c)
	l.Log = append(l.Log, "do   "+c.Label())
	return nil
}

func (l *StockLedger) UndoLast() {
	if len(l.history) == 0 {
		return
	}
	c := l.history[len(l.history)-1]
	l.history = l.history[:len(l.history)-1]
	c.Undo()
	l.Log = append(l.Log, "undo "+c.Label())
}

func main() {
	inv, ledger := NewInventory(), &StockLedger{}
	ledger.Run(&Receive{Inv: inv, SKU: "SKU-7", N: 40})
	ledger.Run(&Pick{Inv: inv, SKU: "SKU-7", N: 12})
	if err := ledger.Run(&Pick{Inv: inv, SKU: "SKU-7", N: 500}); err != nil {
		fmt.Println("refused:", err)
	}
	ledger.Run(&CycleCount{Inv: inv, SKU: "SKU-7", Counted: 25})
	ledger.UndoLast()
	fmt.Println(inv.Qty("SKU-7")) // 28
	fmt.Printf("%q\n", ledger.Log)
}
```

```java title="StockLedgerDemo.java"
import java.util.*;

public class StockLedgerDemo {
    static final class Inventory {
        private final Map<String, Integer> stock = new HashMap<>();
        int qty(String sku) { return stock.getOrDefault(sku, 0); }
        void set(String sku, int n) { stock.put(sku, n); }
    }

    interface StockCommand {
        String label();
        void execute();
        void undo();
    }

    record Receive(Inventory inv, String sku, int n) implements StockCommand {
        public String label() { return "receive " + n + " " + sku; }
        public void execute() { inv.set(sku, inv.qty(sku) + n); }
        public void undo() { inv.set(sku, inv.qty(sku) - n); }
    }

    record Pick(Inventory inv, String sku, int n) implements StockCommand {
        public String label() { return "pick " + n + " " + sku; }
        public void execute() {
            int onHand = inv.qty(sku);
            if (onHand < n) throw new IllegalStateException(label() + ": only " + onHand + " on hand");
            inv.set(sku, onHand - n);
        }
        public void undo() { inv.set(sku, inv.qty(sku) + n); }
    }

    // A class, not a record: it holds mutable state captured at execute time.
    static final class CycleCount implements StockCommand {
        private final Inventory inv; private final String sku; private final int counted;
        private int before;
        CycleCount(Inventory inv, String sku, int counted) { this.inv = inv; this.sku = sku; this.counted = counted; }
        public String label() { return "count " + sku + " = " + counted; }
        public void execute() { before = inv.qty(sku); inv.set(sku, counted); }
        public void undo() { inv.set(sku, before); }
    }

    static final class StockLedger {
        private final Deque<StockCommand> history = new ArrayDeque<>();
        final List<String> log = new ArrayList<>();

        void run(StockCommand cmd) {
            cmd.execute(); // throws before anything is recorded
            history.push(cmd);
            log.add("do   " + cmd.label());
        }

        void undoLast() {
            StockCommand cmd = history.poll();
            if (cmd == null) return;
            cmd.undo();
            log.add("undo " + cmd.label());
        }
    }

    public static void main(String[] args) {
        Inventory inv = new Inventory();
        StockLedger ledger = new StockLedger();
        ledger.run(new Receive(inv, "SKU-7", 40));
        ledger.run(new Pick(inv, "SKU-7", 12));
        try {
            ledger.run(new Pick(inv, "SKU-7", 500));
        } catch (IllegalStateException e) {
            System.out.println("refused: " + e.getMessage());
        }
        ledger.run(new CycleCount(inv, "SKU-7", 25));
        ledger.undoLast();
        System.out.println(inv.qty("SKU-7")); // 28
        ledger.log.forEach(System.out::println);
    }
}
```

## Trade-offs

**Gains**
- Undo, redo, queuing, retry and audit logging all fall out of one abstraction.
- Triggers are decoupled from receivers: one command serves a button, a shortcut and an API route.
- Commands compose into macros and transactions.
- Each command is testable as an `execute` then `undo` round trip.

**Costs**
- One class per operation. A large app can accumulate hundreds of thin command types.
- Undo is only as correct as each command's inverse, and inverses drift when the receiver changes.
- Commands holding live receivers can't be persisted or sent across processes without a separate message format.
- Undo history holds references to receivers and captured state, which can keep memory alive.

## Relations

- [Memento](https://aipatterns.dev/design-patterns/memento.md) stores whole state snapshots. Command stores the delta. Combine them when an operation has no cheap inverse.
- [Strategy](https://aipatterns.dev/design-patterns/strategy.md) also puts behavior in an object, but to choose *how* to compute something. Command chooses *what* to do and when.
- [Chain of Responsibility](https://aipatterns.dev/design-patterns/chain-of-responsibility.md) can pass command objects along its handlers.
- [Mediator](https://aipatterns.dev/design-patterns/mediator.md) and [Observer](https://aipatterns.dev/design-patterns/observer.md) can dispatch commands between components without direct references.
- [Prototype](https://aipatterns.dev/design-patterns/prototype.md) helps when a command must be copied into the history before it is run again.

## Agent checklist

- [ ] There is a concrete need for undo, queuing, logging, retry or replay. Otherwise pass a function.
- [ ] Commands capture undo state in `execute`, not in the constructor.
- [ ] The invoker records a command only after it succeeds.
- [ ] Every command has an `execute` then `undo` round-trip test that compares receiver state before and after.
- [ ] If commands cross a process boundary, they are plain serializable data, not objects holding receivers.
