Marcelo Pastorino, Software Developer

Command Pattern

A behavioral design pattern that encapsulates a request as an object, so callers can queue, log, undo, or defer execution without knowing how the work is done.

The command pattern is a behavioral design pattern that turns a request into a stand-alone object with all the information needed to perform it. A client builds a command and hands it to an invoker. The invoker triggers execute() without knowing which receiver will do the work or how. That separation lets you treat operations like data: queue them, log them, schedule them, or reverse them with undo().

The problem it solves

UI toolbars, remote controls, and workflow engines often need to trigger the same action from many places, such as a button, a keyboard shortcut, a macro, or an API call. If each entry point calls the receiver directly, you duplicate wiring, couple callers to implementation details, and lose a single place to add cross-cutting behavior such as history, validation, or auditing.

The command pattern wraps each action in its own object. Callers depend on a small command interface; the concrete command holds the receiver and the parameters. New actions are new classes, and the invoker stays unchanged.

Structure

  • Command, declares execute() (and often undo()).
  • Concrete command, binds a receiver, stores request parameters, and delegates execution.
  • Receiver, knows how to perform the actual work.
  • Invoker, holds a command and calls execute() when triggered; may track history for undo/redo.
  • Client, creates concrete commands and wires them to receivers and invokers.
Invoker runs commands without knowing receiver details

In code

A text editor stores edits as command objects. The toolbar invokes commands; the buffer performs the mutation. Undo pops the last command and calls undo() on it.

// Receiver: knows how to mutate state
class TextBuffer {
  constructor() {
    this.content = "";
  }
  insert(text) {
    this.content += text;
  }
  delete(count) {
    this.content = this.content.slice(0, -count);
  }
  getContent() {
    return this.content;
  }
}

// Concrete command: binds receiver + parameters
class InsertTextCommand {
  constructor(buffer, text) {
    this.buffer = buffer;
    this.text = text;
  }
  execute() {
    this.buffer.insert(this.text);
  }
  undo() {
    this.buffer.delete(this.text.length);
  }
}

// Invoker: triggers commands and tracks history
class Editor {
  constructor() {
    this.history = [];
    this.undone = [];
  }
  run(command) {
    command.execute();
    this.history.push(command);
    this.undone = [];
  }
  undo() {
    const command = this.history.pop();
    if (!command) return;
    command.undo();
    this.undone.push(command);
  }
  redo() {
    const command = this.undone.pop();
    if (!command) return;
    command.execute();
    this.history.push(command);
  }
}

const buffer = new TextBuffer();
const editor = new Editor();
editor.run(new InsertTextCommand(buffer, "hello"));
editor.run(new InsertTextCommand(buffer, " world"));
console.log(buffer.getContent()); // hello world
editor.undo();
console.log(buffer.getContent()); // hello
// Command: shared interface
interface Command {
  execute(): void;
  undo(): void;
}

// Receiver: knows how to mutate state
class TextBuffer {
  private content = "";
  insert(text: string): void {
    this.content += text;
  }
  delete(count: number): void {
    this.content = this.content.slice(0, -count);
  }
  getContent(): string {
    return this.content;
  }
}

// Concrete command: binds receiver + parameters
class InsertTextCommand implements Command {
  constructor(
    private readonly buffer: TextBuffer,
    private readonly text: string
  ) {}
  execute(): void {
    this.buffer.insert(this.text);
  }
  undo(): void {
    this.buffer.delete(this.text.length);
  }
}

// Invoker: triggers commands and tracks history
class Editor {
  private history: Command[] = [];
  private undone: Command[] = [];
  run(command: Command): void {
    command.execute();
    this.history.push(command);
    this.undone = [];
  }
  undo(): void {
    const command = this.history.pop();
    if (!command) return;
    command.undo();
    this.undone.push(command);
  }
  redo(): void {
    const command = this.undone.pop();
    if (!command) return;
    command.execute();
    this.history.push(command);
  }
}

const buffer = new TextBuffer();
const editor = new Editor();
editor.run(new InsertTextCommand(buffer, "hello"));
editor.run(new InsertTextCommand(buffer, " world"));
console.log(buffer.getContent()); // hello world
editor.undo();
console.log(buffer.getContent()); // hello
// Command: shared interface
public interface ICommand
{
    void Execute();
    void Undo();
}

// Receiver: knows how to mutate state
public class TextBuffer
{
    private string _content = "";
    public void Insert(string text) => _content += text;
    public void Delete(int count) => _content = _content[..^count];
    public string GetContent() => _content;
}

// Concrete command: binds receiver + parameters
public class InsertTextCommand : ICommand
{
    private readonly TextBuffer _buffer;
    private readonly string _text;
    public InsertTextCommand(TextBuffer buffer, string text)
    {
        _buffer = buffer;
        _text = text;
    }
    public void Execute() => _buffer.Insert(_text);
    public void Undo() => _buffer.Delete(_text.Length);
}

// Invoker: triggers commands and tracks history
public class Editor
{
    private readonly Stack<ICommand> _history = new();
    private readonly Stack<ICommand> _undone = new();
    public void Run(ICommand command)
    {
        command.Execute();
        _history.Push(command);
        _undone.Clear();
    }
    public void Undo()
    {
        if (_history.Count == 0) return;
        var command = _history.Pop();
        command.Undo();
        _undone.Push(command);
    }
    public void Redo()
    {
        if (_undone.Count == 0) return;
        var command = _undone.Pop();
        command.Execute();
        _history.Push(command);
    }
}

var buffer = new TextBuffer();
var editor = new Editor();
editor.Run(new InsertTextCommand(buffer, "hello"));
editor.Run(new InsertTextCommand(buffer, " world"));
Console.WriteLine(buffer.GetContent()); // hello world
editor.Undo();
Console.WriteLine(buffer.GetContent()); // hello
from typing import Protocol

# Command: shared interface
class Command(Protocol):
    def execute(self) -> None: ...
    def undo(self) -> None: ...

# Receiver: knows how to mutate state
class TextBuffer:
    def __init__(self) -> None:
        self._content = ""
    def insert(self, text: str) -> None:
        self._content += text
    def delete(self, count: int) -> None:
        self._content = self._content[:-count]
    def get_content(self) -> str:
        return self._content

# Concrete command: binds receiver + parameters
class InsertTextCommand:
    def __init__(self, buffer: TextBuffer, text: str) -> None:
        self._buffer = buffer
        self._text = text
    def execute(self) -> None:
        self._buffer.insert(self._text)
    def undo(self) -> None:
        self._buffer.delete(len(self._text))

# Invoker: triggers commands and tracks history
class Editor:
    def __init__(self) -> None:
        self._history: list[Command] = []
        self._undone: list[Command] = []
    def run(self, command: Command) -> None:
        command.execute()
        self._history.append(command)
        self._undone.clear()
    def undo(self) -> None:
        if not self._history:
            return
        command = self._history.pop()
        command.undo()
        self._undone.append(command)
    def redo(self) -> None:
        if not self._undone:
            return
        command = self._undone.pop()
        command.execute()
        self._history.append(command)

buffer = TextBuffer()
editor = Editor()
editor.run(InsertTextCommand(buffer, "hello"))
editor.run(InsertTextCommand(buffer, " world"))
print(buffer.get_content())  # hello world
editor.undo()
print(buffer.get_content())  # hello

Undo, queues, and macros

Commands become powerful when you treat them as first-class values:

  • Undo/redo, store executed commands in a stack; undo() reverses the last one. Composite commands can group several operations into a single undo step.
  • Queues and scheduling, an invoker can enqueue commands and drain them later (batch jobs, deferred UI work, worker threads).
  • Logging and replay, serializing commands records intent, not just side effects, which helps auditing and deterministic replay.
  • Macros, a macro command holds a list of child commands and runs them in order as one unit.

Not every command needs undo(). Read-only queries, fire-and-forget notifications, or irreversible side effects (sending email) often expose only execute().

Relationship to SOLID

Command applies the SOLID principles. It supports the open/closed principle because new operations are new command classes, not edits to the invoker. It follows single responsibility by isolating each request in its own object. It also aligns with dependency inversion when callers and invokers depend on the command abstraction instead of concrete receivers.

Command vs alternatives

  • Strategy pattern, also encapsulates behavior behind an interface, but a strategy usually represents a long-lived algorithm the context holds. A command represents a single request with parameters, often created, executed once, and discarded or logged.
  • State pattern, an object delegates behavior to a state that may change as the context evolves. Command objects carry an action to perform; they do not replace the object’s ongoing behavior mode.
  • Observer pattern, broadcasts that something happened. Command expresses an intention to make something happen and can be stored before execution.
  • Event-driven architecture, message types and handlers across services mirror command/query separation at scale. In-process commands are the local version; distributed systems persist and route command messages through brokers.
  • Plain functions or lambdas, when you only need deferred execution without undo, logging, or polymorphic dispatch, a closure is often enough. Reach for command objects when history, composition, or uniform invocation matter.

Trade-offs

  • One class per action adds files and ceremony. For a handful of simple callbacks, direct method calls stay clearer.
  • Undo requires reversible operations or compensating logic. Destructive or external side effects need careful design (sagas, idempotent compensations) rather than a naive stack pop.
  • Macro and composite commands must define clear boundaries. Partial failure mid-macro complicates undo unless you model transactions or rollback steps explicitly.
  • Best when the same operation is triggered from many places, when you need undo/redo or audit trails, when work should be queued or retried, or when you want a stepping stone toward command messages in event-driven architecture.

See also

  • Composite Pattern, A structural design pattern that models tree structures so clients treat individual objects and groups of objects the same way through a shared component interface.
  • Strategy Pattern, A behavioral design pattern that encapsulates interchangeable algorithms behind a common interface, selected at runtime.
  • State Pattern, A behavioral design pattern that lets an object change its behavior when its internal state changes, by delegating to interchangeable state objects.
  • Observer Pattern, A behavioral design pattern that lets dependents subscribe to a subject and receive automatic updates when its state changes, without tight coupling between them.
  • Event-Driven Architecture, Decoupling producers and consumers through asynchronous messaging.
  • SOLID Principles, Five object-oriented design principles for maintainable, change-tolerant code.

On Wikipedia