Design Patterns, Explained Simply · Behavioral Patterns: How Objects Share Work
Command
Turn a request into an object, so you can queue it, log it, retry it and, most usefully, undo it.
The problem
A text editor's toolbar buttons call editor methods directly: the "Bold" button calls editor.bold(), the "Delete" button calls editor.delete(). Now users want undo and redo, a macro feature that replays a sequence of actions, and keyboard shortcuts that do the same thing as buttons.
With direct calls, there's no record of what happened, so nothing to undo or replay. Each button and shortcut duplicates the logic.
Real-world analogy
A restaurant order slip. The waiter doesn't cook; they write the order on a slip (the command) and put it in a queue. The kitchen executes slips in order. The slip can be cancelled, kept as a record, or repeated ("same again").
Without the pattern
def on_type_clicked(text):
editor.content += text # no record of what happened, so nothing to undo
def on_delete_clicked(n):
editor.content = editor.content[:-n]
The pattern
- Command interface:
execute()and, for undo,undo(). - Concrete commands (
TypeText,DeleteText): store everything needed to perform the action and to reverse it. - Receiver (
Document): the object that does the real work. - Invoker (
Editorwith a history): runs commands and keeps them in a history stack for undo/redo, without knowing what each command does.
The code
class Document: # receiver
def __init__(self):
self.text = ""
class TypeText:
def __init__(self, doc, text):
self.doc, self.text = doc, text
def execute(self):
self.doc.text += self.text
def undo(self):
self.doc.text = self.doc.text[: len(self.doc.text) - len(self.text)]
class DeleteLast:
def __init__(self, doc, count):
self.doc, self.count, self.removed = doc, count, ""
def execute(self):
self.removed = self.doc.text[-self.count:] # remember what to restore
self.doc.text = self.doc.text[: -self.count]
def undo(self):
self.doc.text += self.removed
class Editor: # invoker: runs commands and keeps the history
def __init__(self):
self.done, self.undone = [], []
def run(self, command):
command.execute()
self.done.append(command)
self.undone.clear()
def undo(self):
command = self.done.pop()
command.undo()
self.undone.append(command)
def redo(self):
command = self.undone.pop()
command.execute()
self.done.append(command)
doc, editor = Document(), Editor()
editor.run(TypeText(doc, "Hello"))
editor.run(TypeText(doc, ", world"))
editor.run(DeleteLast(doc, 5))
print(f"after edits: '{doc.text}'")
editor.undo()
print(f"undo: '{doc.text}'")
editor.undo()
print(f"undo: '{doc.text}'")
editor.redo()
print(f"redo: '{doc.text}'")
Output
after edits: 'Hello, ' undo: 'Hello, world' undo: 'Hello' redo: 'Hello, world'
interface Command {
execute(): void;
undo(): void;
}
class Doc { // receiver
text = "";
}
class TypeText implements Command {
constructor(private doc: Doc, private text: string) {}
execute() { this.doc.text += this.text; }
undo() { this.doc.text = this.doc.text.slice(0, this.doc.text.length - this.text.length); }
}
class DeleteLast implements Command {
private removed = "";
constructor(private doc: Doc, private count: number) {}
execute() {
this.removed = this.doc.text.slice(-this.count); // remember what to restore
this.doc.text = this.doc.text.slice(0, -this.count);
}
undo() { this.doc.text += this.removed; }
}
class Editor { // invoker: runs commands and keeps the history
private done: Command[] = [];
private undone: Command[] = [];
run(command: Command) {
command.execute();
this.done.push(command);
this.undone = [];
}
undo() {
const command = this.done.pop()!;
command.undo();
this.undone.push(command);
}
redo() {
const command = this.undone.pop()!;
command.execute();
this.done.push(command);
}
}
const doc = new Doc();
const editor = new Editor();
editor.run(new TypeText(doc, "Hello"));
editor.run(new TypeText(doc, ", world"));
editor.run(new DeleteLast(doc, 5));
console.log(`after edits: '${doc.text}'`);
editor.undo();
console.log(`undo: '${doc.text}'`);
editor.undo();
console.log(`undo: '${doc.text}'`);
editor.redo();
console.log(`redo: '${doc.text}'`);
Output
after edits: 'Hello, ' undo: 'Hello, world' undo: 'Hello' redo: 'Hello, world'
import java.util.ArrayDeque;
import java.util.Deque;
public class Main {
public static void main(String[] args) {
Doc doc = new Doc();
Editor editor = new Editor();
editor.run(new TypeText(doc, "Hello"));
editor.run(new TypeText(doc, ", world"));
editor.run(new DeleteLast(doc, 5));
System.out.println("after edits: '" + doc.text + "'");
editor.undo();
System.out.println("undo: '" + doc.text + "'");
editor.undo();
System.out.println("undo: '" + doc.text + "'");
editor.redo();
System.out.println("redo: '" + doc.text + "'");
}
}
interface Command {
void execute();
void undo();
}
class Doc { // receiver
String text = "";
}
class TypeText implements Command {
private final Doc doc;
private final String text;
TypeText(Doc doc, String text) { this.doc = doc; this.text = text; }
public void execute() { doc.text += text; }
public void undo() { doc.text = doc.text.substring(0, doc.text.length() - text.length()); }
}
class DeleteLast implements Command {
private final Doc doc;
private final int count;
private String removed = "";
DeleteLast(Doc doc, int count) { this.doc = doc; this.count = count; }
public void execute() {
removed = doc.text.substring(doc.text.length() - count); // remember what to restore
doc.text = doc.text.substring(0, doc.text.length() - count);
}
public void undo() { doc.text += removed; }
}
class Editor { // invoker: runs commands and keeps the history
private final Deque<Command> done = new ArrayDeque<>();
private final Deque<Command> undone = new ArrayDeque<>();
void run(Command command) {
command.execute();
done.push(command);
undone.clear();
}
void undo() {
Command command = done.pop();
command.undo();
undone.push(command);
}
void redo() {
Command command = undone.pop();
command.execute();
done.push(command);
}
}
Output
after edits: 'Hello, ' undo: 'Hello, world' undo: 'Hello' redo: 'Hello, world'
Run this on your computer — see “Get Started” for setup.
<?php
interface Command {
public function execute(): void;
public function undo(): void;
}
class Doc { // receiver
public string $text = "";
}
class TypeText implements Command {
public function __construct(private Doc $doc, private string $text) {}
public function execute(): void { $this->doc->text .= $this->text; }
public function undo(): void { $this->doc->text = substr($this->doc->text, 0, strlen($this->doc->text) - strlen($this->text)); }
}
class DeleteLast implements Command {
private string $removed = "";
public function __construct(private Doc $doc, private int $count) {}
public function execute(): void {
$this->removed = substr($this->doc->text, -$this->count); // remember what to restore
$this->doc->text = substr($this->doc->text, 0, -$this->count);
}
public function undo(): void { $this->doc->text .= $this->removed; }
}
class Editor { // invoker: runs commands and keeps the history
private array $done = [];
private array $undone = [];
public function run(Command $command): void {
$command->execute();
$this->done[] = $command;
$this->undone = [];
}
public function undo(): void {
$command = array_pop($this->done);
$command->undo();
$this->undone[] = $command;
}
public function redo(): void {
$command = array_pop($this->undone);
$command->execute();
$this->done[] = $command;
}
}
$doc = new Doc();
$editor = new Editor();
$editor->run(new TypeText($doc, "Hello"));
$editor->run(new TypeText($doc, ", world"));
$editor->run(new DeleteLast($doc, 5));
echo "after edits: '{$doc->text}'\n";
$editor->undo();
echo "undo: '{$doc->text}'\n";
$editor->undo();
echo "undo: '{$doc->text}'\n";
$editor->redo();
echo "redo: '{$doc->text}'\n";
Output
after edits: 'Hello, ' undo: 'Hello, world' undo: 'Hello' redo: 'Hello, world'
The editor's history knows nothing about typing or deleting; it just calls execute() and undo(). A new action (bold, paste) is a new command class.
When to use it
- Undo/redo, history and macros.
- Queues and background jobs: a job is a command object serialised to a queue and executed later by a worker.
- Retries and audit logs: commands can be stored, replayed or logged.
- Decoupling UI elements (buttons, menus, shortcuts) from the operations they trigger.
When not to use it
- A simple callback is enough and you don't need undo, queuing or history. In Python and TypeScript a function or lambda is a lightweight command.
- Undo is hard for actions with external side effects (sending an email, charging a card). Those need compensating actions (refund, follow-up email), not a simple
undo().
You've already used it
- Job queues: Laravel jobs, Celery tasks, Sidekiq and BullMQ jobs are serialisable command objects.
- Redux: actions are command-like objects describing what happened; reducers apply them.
- CQRS: the "C" stands for commands, such as
PlaceOrderobjects handled by command handlers. - Java:
RunnableandCallablepassed to an executor.
Key points
- Command turns a request into an object with
execute()(and oftenundo()). - Commands can be queued, logged, retried, replayed and undone.
- The invoker runs commands without knowing what they do.
- Side effects in the outside world need compensating actions, not a naive undo.
- Further reading: Refactoring Guru: Command.
Practice questions
How does the Command pattern make undo and redo possible?
Answer
Each action becomes an object that stores what it needs to execute and reverse itself. An invoker keeps executed commands on a history stack; undo pops the last command and calls undo(), redo re-executes it.
How do background job queues relate to the Command pattern?
Answer
A queued job is a command object: it holds the data needed to perform an action and has a handle()/execute() method. It is serialised to a queue and executed later by a worker, possibly retried. Laravel jobs and Celery tasks are examples.
Why is undo difficult for commands with external side effects?
Answer
You can't "un-send" an email or simply reverse a card charge. Such commands need compensating actions (a refund, a correction email) instead of restoring previous state.
Open in the app to mark lessons complete, take notes and track your progress.