Command Pattern
1. What the command pattern is
The command pattern turns a request into an object. Instead of calling a method directly, you build an object that holds the call (which method, on which receiver, with which arguments) and run it later. The code that triggers the call stops needing to know what the call actually does.
It has four roles:
- Command — an interface with a single
execute()method (often anundo()alongside it). - Receiver — the object that knows how to do the real work. The command delegates to it.
- Invoker — holds a command and calls
execute()at some point. It never knows which command. - Client — wires a concrete command to its receiver and hands it to the invoker.
The shape is small. What’s interesting is what falls out of it once a call is a value you can hold.
2. Why turn a method call into an object
A plain method call happens once, immediately, where you wrote it. That’s usually fine, and most calls should stay that way. The command pattern earns its keep when you need a call to be something you can keep around: store it, pass it, queue it, log it, reverse it. A method invocation in Java isn’t a value. You can’t put editor.insert("x") into a list and run it tomorrow. An object that represents that call, you can.
Concretely, wrapping a call in an object buys you four things:
- Decoupling. The invoker (a button, a menu item, a scheduler) holds a
Commandand nothing else. Add a new operation and the invoker doesn’t change. - Deferral and queuing. Commands are objects, so a queue or a thread pool can hold thousands of them and run them whenever it wants.
- Logging and replay. A list of executed commands is an audit trail. Persist it and you can replay it to rebuild state.
- Undo. If a command knows how to reverse itself, a history of commands becomes an undo stack.
Undo is the one that is genuinely hard to get any other way, so it gets its own section (§4). The other three you can approximate with a bare Runnable or a lambda, which is what §5 is about.
3. Writing one in Java
The example is a tiny text editor. The receiver is the document; commands are edits to it.
3.1. The receiver and the Command interface
The receiver does the actual work and knows nothing about commands:
public class Document {
private final StringBuilder text = new StringBuilder();
public void insert(String s) {
text.append(s);
}
public void delete(int length) {
int from = Math.max(0, text.length() - length);
text.delete(from, text.length());
}
public String tail(int length) {
int from = Math.max(0, text.length() - length);
return text.substring(from);
}
public String content() {
return text.toString();
}
}The command interface is one method:
public interface Command {
void execute();
}We add undo() in §4. Starting without it keeps the first version honest about how little the pattern needs to get going.
3.2. Concrete commands
A concrete command binds a receiver and the arguments for exactly one operation:
public class InsertCommand implements Command {
private final Document document;
private final String textToInsert;
public InsertCommand(Document document, String textToInsert) {
this.document = document;
this.textToInsert = textToInsert;
}
@Override
public void execute() {
document.insert(textToInsert);
}
}DeleteCommand is the same shape with an int length in place of the string. Notice that execute() takes no parameters: the command already carries everything the call needs, and that’s what lets the invoker run it without knowing anything about it.
3.3. The invoker
The invoker holds commands and runs them. Here it is the editor itself, exposing one entry point that anything (a key binding, a macro, a replayed script) routes through:
public class Editor {
public void run(Command command) {
command.execute();
}
}The client wires the pieces together:
Document doc = new Document();
Editor editor = new Editor();
editor.run(new InsertCommand(doc, "Hello"));
editor.run(new InsertCommand(doc, ", world"));
editor.run(new DeleteCommand(doc, 7));
System.out.println(doc.content()); // HelloEditor never mentions Document, InsertCommand or DeleteCommand. That’s the decoupling from §2: the invoker depends on the Command interface and nothing else.
run looks trivial at this stage, and it is. But it is the single point where undo, history, and macros all attach in the next section, and none of them touch the command classes.
4. Undo and redo
Undo is the reason most teams reach for the command pattern. The mechanism is two halves: each command knows how to reverse itself, and the invoker keeps a history of what ran.
First, add undo() to the interface:
public interface Command {
void execute();
void undo();
}InsertCommand reverses easily. To undo an insert, delete what you inserted:
@Override
public void undo() {
document.delete(textToInsert.length());
}DeleteCommand is the instructive one. To undo a delete you need the deleted text, and that text does not exist until execute() actually runs. So the command captures it at execution time, not at construction:
public class DeleteCommand implements Command {
private final Document document;
private final int length;
private String deletedText; // captured at execute()
public DeleteCommand(Document document, int length) {
this.document = document;
this.length = length;
}
@Override
public void execute() {
deletedText = document.tail(length);
document.delete(length);
}
@Override
public void undo() {
document.insert(deletedText);
}
}This is the design choice worth naming. A command can store its reversal state either at construction time or at execution time, and the two aren’t interchangeable. Insert knows its reversal up front. Delete can’t, because the text to restore depends on the document’s state at the moment the command runs, which depends on every command before it. Capturing in execute() is what makes undo correct no matter when the command actually runs.
Then the invoker keeps two stacks, one for history and one for redo:
public class Editor {
private final Deque<Command> history = new ArrayDeque<>();
private final Deque<Command> redoStack = new ArrayDeque<>();
public void run(Command command) {
command.execute();
history.push(command);
redoStack.clear(); // a fresh action invalidates the redo branch
}
public void undo() {
if (history.isEmpty()) {
return;
}
Command command = history.pop();
command.undo();
redoStack.push(command);
}
public void redo() {
if (redoStack.isEmpty()) {
return;
}
Command command = redoStack.pop();
command.execute();
history.push(command);
}
}Two details make this behave like a real editor. redoStack.clear() on every fresh run discards the redo branch the moment you undo a few steps and then do something new — which is what every editor does. redo() is just execute() called again; that’s correct only because the command captured its reversal state on the first run and execute() was written to be repeatable.
Macros come along for free, because a command that holds a list of commands is itself a Command:
public class MacroCommand implements Command {
private final List<Command> commands;
public MacroCommand(List<Command> commands) {
this.commands = List.copyOf(commands);
}
@Override
public void execute() {
for (Command c : commands) {
c.execute();
}
}
@Override
public void undo() {
for (int i = commands.size() - 1; i >= 0; i--) {
commands.get(i).undo();
}
}
}The invoker can’t tell a MacroCommand from an InsertCommand, so undoing a macro undoes all its steps as a single history entry. Note the reverse iteration in undo(): steps have to be unwound in the opposite order they were applied. That’s the composite pattern riding along on the command interface.
5. Commands as functions: Runnable and lambdas
Runnable is already a command interface; its method just happens to be named run() instead of execute(). When a command needs only that one method (no undo, no history, no macros), a full class per operation is overkill and a lambda does the job.
Runnable insert = () -> doc.insert("Hello");
Runnable delete = () -> doc.delete(5);
insert.run();
delete.run();A lambda closes over the receiver and the arguments the same way a concrete command stores them in fields: doc and the literals are captured instead of assigned. For a queue of fire-and-forget tasks, this is the command pattern with none of the class boilerplate. It’s also why ExecutorService.submit(Runnable) is a command invoker in everything but name: it takes a request as an object and runs it later, on a thread of its choosing.
The class-based version earns its boilerplate back the moment a command has to be more than callable. undo() is a second method, and a functional interface has room for exactly one. A lambda also can’t hold per-instance mutable state cleanly (the deletedText that DeleteCommand captures during execute()), can’t be type-switched, and shows up as a meaningless synthetic name in a stack trace. Rule of thumb: a lambda when the command is just a deferred call, a class when the command is a thing with state and a reverse gear.
6. When not to reach for it
The command pattern adds a class, sometimes several, per operation. That’s a real cost, worth paying only when you actually use what the indirection buys. A few smells that say you don’t:
- The command runs exactly once, right where it is created.
new SaveCommand(doc).execute()on the next line is a method call with extra steps. Calldoc.save(). - Nothing holds the command. No queue, no history, no invoker fed from more than one source. Then there is no invoker to decouple, and the interface buys nothing.
- You need
execute()and nothing else. UseRunnableor a lambda (§5). A bespoke single-method interface per project is reinventingRunnablewith a worse name.
The pattern pays off when a request genuinely needs to outlive the moment it’s issued, so it can be queued, logged, replayed, or undone. When you find yourself adding undo(), building a history, or feeding one invoker from several sources, you’re using the pattern for its purpose. Short of that, a method call is a method call.