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 oftenundo()). - 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.
flowchart LR Client["Client"] -->|"creates"| InsertCmd["InsertTextCommand"] Client -->|"creates"| DeleteCmd["DeleteTextCommand"] Invoker["Editor (Invoker)"] -->|"execute()"| InsertCmd Invoker -->|"execute()"| DeleteCmd InsertCmd -->|"delegates to"| Buffer["TextBuffer (Receiver)"] DeleteCmd -->|"delegates to"| Buffer
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()); // hellofrom 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()) # helloUndo, 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.
