Command Pattern: Encapsulating Requests as Objects
Turn an action into an object you can queue, log, undo, and replay — the pattern behind text editor undo stacks, job queues, and transactional workflows.
Command Pattern
Encapsulate a request as an object, so it can be parameterized, queued, logged, and undone — independent of the code that triggers it and the code that carries it out. Where Strategy encapsulates how to compute something, Command encapsulates an action to be performed, complete with everything needed to perform (and reverse) it.
The three roles that make this work: the Invoker (triggers the command, doesn't know what it does), the Command (encapsulates the action + its parameters), and the Receiver (the object that actually does the work).
1. The Problem: Invoker Coupled to Concrete Actions
A "smart" remote control button that directly calls light.on() can't be reprogrammed, can't queue a sequence of actions, can't log what happened, and definitely can't undo it.
// VIOLATION: invoker directly coupled to receiver's concrete API
class RemoteButton {
private Light light;
RemoteButton(Light light) { this.light = light; }
void press() {
light.turnOn(); // hardcoded action — this button can only ever do this one thing
// No way to: queue this for later, log it, undo it, or
// reprogram this button to control a Fan instead without editing this class.
}
}
class Light {
void turnOn() { System.out.println("Light ON"); }
void turnOff() { System.out.println("Light OFF"); }
}Reprogramming this button to control a Fan instead means rewriting RemoteButton itself. There is no way to record "what just happened" for an undo stack, because the action was never captured as data — it was just a direct method call that already happened and left no trace.
2. Structure
The invoker (RemoteControl) never references Light at all — it only knows the Command interface. This is what makes the button reprogrammable, loggable, and undoable: the action is data, not a hardcoded call.
3. Full Implementation
// The Receiver — the object that actually does the work
class Light {
private boolean isOn = false;
void turnOn() { isOn = true; System.out.println("Light ON"); }
void turnOff() { isOn = false; System.out.println("Light OFF"); }
}
// The Command interface — every action supports execute AND its inverse
interface Command {
void execute();
void undo();
}
class LightOnCommand implements Command {
private final Light light;
LightOnCommand(Light light) { this.light = light; }
@Override
public void execute() { light.turnOn(); }
@Override
public void undo() { light.turnOff(); } // inverse of execute
}
class LightOffCommand implements Command {
private final Light light;
LightOffCommand(Light light) { this.light = light; }
@Override
public void execute() { light.turnOff(); }
@Override
public void undo() { light.turnOn(); }
}
// The Invoker — knows only the Command interface, never the receiver
class RemoteControl {
private final Deque<Command> history = new ArrayDeque<>();
void pressButton(Command command) {
command.execute();
history.push(command); // record for undo
}
void pressUndo() {
if (!history.isEmpty()) {
history.pop().undo();
}
}
}
class Demo {
public static void main(String[] args) {
Light livingRoomLight = new Light();
RemoteControl remote = new RemoteControl();
remote.pressButton(new LightOnCommand(livingRoomLight)); // Light ON
remote.pressButton(new LightOffCommand(livingRoomLight)); // Light OFF
remote.pressUndo(); // Light ON (undoes the OFF)
remote.pressUndo(); // Light OFF (undoes the ON)
// Reprogramming the remote to control a Fan needs zero changes to RemoteControl —
// just a new Command implementation.
}
}4. Macro Commands: Composing Commands
Because every Command shares the same interface, a MacroCommand can hold a list of commands and treat them as one — the Composite pattern applied to Command.
// A macro command: one press executes an entire sequence, undo reverses it in order
class MacroCommand implements Command {
private final List<Command> commands;
MacroCommand(List<Command> commands) { this.commands = commands; }
@Override
public void execute() {
for (Command command : commands) command.execute();
}
@Override
public void undo() {
// undo in REVERSE order — critical for correctness when commands depend on each other
for (int i = commands.size() - 1; i >= 0; i--) {
commands.get(i).undo();
}
}
}
// "Good night" scene: one button press, several receivers
Command goodNight = new MacroCommand(List.of(
new LightOffCommand(livingRoomLight),
new LightOffCommand(bedroomLight),
new LockDoorCommand(frontDoor)
));
remote.pressButton(goodNight); // executes all three
remote.pressUndo(); // reverses all three, in reverse orderUndoing in reverse order matters whenever later commands depend on the state earlier commands established (e.g., "reserve seat" then "charge card" — undo must refund before un-reserving, or in whatever order correctly reverses the dependency).
5. Command for Transaction-Style Workflows
Command's execute/undo symmetry generalizes naturally to multi-step business transactions where a failure partway through needs to roll back everything that already succeeded — the same shape as a manual saga.
interface TransactionalCommand {
void execute() throws Exception;
void rollback();
}
class TransactionRunner {
private final Deque<TransactionalCommand> completed = new ArrayDeque<>();
void run(List<TransactionalCommand> steps) {
try {
for (TransactionalCommand step : steps) {
step.execute();
completed.push(step); // only tracked after success
}
} catch (Exception e) {
System.err.println("Transaction failed, rolling back: " + e.getMessage());
while (!completed.isEmpty()) {
completed.pop().rollback(); // undo only what actually succeeded
}
}
}
}
// Steps like ReserveInventoryCommand, ChargeCardCommand, CreateShipmentCommand
// each implement execute()/rollback() — if ChargeCardCommand throws,
// only ReserveInventoryCommand needs rollback(), and the runner knows that
// precisely because it tracked what actually completed.6. When to Use vs. When It's Overkill
| Use Command when | Skip it when |
|---|---|
| You need undo/redo, or an action needs to be logged/replayed | The action is a one-shot, non-reversible call with no queuing or history need |
| The invoker (UI button, scheduler, queue) should be decoupled from what it triggers | Invoker and receiver are tightly related and will never vary independently |
| You need to queue actions, execute them later, or on a different thread | Immediate, synchronous execution is all that's ever needed |
| Multiple related actions need to be composed and treated as one unit (macro) | There's only ever a single, non-composable action |
Over-applying Command: wrapping a single, permanent, non-undoable button-to-method call in a Command interface with one implementation adds a class and an interface for a relationship that will never need to vary, queue, or reverse. Reserve Command for actions that genuinely need to be treated as data — deferred, logged, undone, or composed.
7. Command vs. Chain of Responsibility
Both patterns are frequently taught as a pair because they touch "who handles a request," but they solve different halves of the problem:
| Command | Chain of Responsibility | |
|---|---|---|
| Encapsulates | One action as an object — execute (and often undo) it | Routing of a request through a sequence of potential handlers |
| Who acts | The receiver (invoked by exactly one command) | Exactly one handler in the chain typically processes it (or several, in the "pass-through" variant) |
| Primary use case | Undo/redo, queuing, logging, macro composition, transactions | Middleware pipelines, escalation chains, request filtering |
| Shape | Invoker → Command → Receiver (linear delegation) | Handler → next Handler → next Handler (a chain, decided at runtime) |
The two compose well: each node in a Chain of Responsibility can internally use Command objects to represent "what to do if I handle this request." See Chain of Responsibility for the routing half of this pairing.
8. Real-World / Production Examples
- Text editor undo/redo — every keystroke/formatting action is a
Command; the undo stack is literally a stack of executed commands. java.lang.Runnable/Callable— the JDK's minimal Command: an action wrapped as an object, handed to anExecutorServiceinvoker that doesn't know or care what the action does.- Job queues (Sidekiq, Celery, Spring's
@Async+TaskExecutor) — a job is a serialized Command; workers are invokers that execute whatever command they dequeue. - Database transactions with compensating actions (Sagas) — each step in a distributed transaction is a Command with an
execute/compensatepair, exactly the transactional pattern shown above. - GUI frameworks (Swing
Action, WPFICommand) — menu items and buttons bind to aCommandobject rather than a hardcoded handler, enabling the same action to be triggered from a menu, a toolbar, and a keyboard shortcut. - CQRS command handlers — in CQRS architectures, a "command" (
PlaceOrderCommand) is dispatched to exactly one handler, formalizing invoker/command/receiver at the architecture level.
Interview Questions
- What are the three roles in Command (invoker, command, receiver), and why does decoupling them matter for undo/redo?
- How would you implement a
MacroCommand? Why must itsundo()reverse the order of itsexecute()? - Explain how Command supports a transactional workflow with partial rollback. What does the runner need to track, and why only after success?
- Compare
Runnablein the JDK to the Command pattern. What role doesExecutorServiceplay? - What's the difference between Command and Chain of Responsibility? Could you use both together — how?
- Why is it important that a Command object capture all the state (parameters) needed to both execute and undo, rather than referencing external mutable state?
- Describe a real system where Command enables replaying a sequence of actions (e.g., event sourcing, audit logs). What does "replay" require of each command?
- When would introducing the Command pattern be unnecessary ceremony for a simple button-click handler?