7 min read
Object-Oriented Design Index
Tier 1 -- Foundations
Core Concepts
SOLID Principles
Creational Patterns
Structural Patterns
Tier 2
Tier 3
Tier 4
Tier 5
Object-Oriented Design Index
Tier 1 -- Foundations
Core Concepts
SOLID Principles
Creational Patterns
Structural Patterns
Tier 2
Tier 3
Tier 4
Tier 5
Command Pattern
1. What Is It?
The pattern encapsulates a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
Without it, action logic is either hard-coded in the invoker (tight between "who triggers" and "what executes") or scattered across ad hoc callbacks. You can't queue actions, log them for replay, or undo them without invasive changes. turns each request into a first-class object that can be stored, passed around, composed, and reversed.
A text editor currently handles 'Bold', 'Italic', and 'Undo' actions by calling formatting functions directly from button click handlers. The team now wants to add a macro-recording feature that replays a sequence of user actions. Why does the current design make this difficult, and what does the Command pattern offer to address it?
2. How It Works
Participants:
- — with
execute()and optionallyundo() - ConcreteCommand — binds a specific action to a specific Receiver; implements
execute()by calling receiver methods; stores enough to implementundo() - Receiver — the object that knows how to perform the actual work (e.g.,
TextEditor,Light,FileSystem) - Invoker — asks the to carry out the request; may queue commands, log them, or call
undo(); does not know what the command does or who the receiver is - Client — creates ConcreteCommand objects, wires them to Receivers, and passes them to the Invoker
Step-by-step:
- Define
withexecute()(andundo()if reversibility is needed) - Implement
ConcreteCommand— stores a reference to its Receiver and any parameters needed;execute()delegates to Receiver Invokerholds a command (or a queue); callsexecute()without knowing the details- Client wires:
command = ConcreteCommand(receiver, args); invoker.set_command(command)
Python:
from __future__ import annotations from abc import ABC, abstractmethod from collections import deque class Command(ABC): @abstractmethod def execute(self) -> None: pass @abstractmethod def undo(self) -> None: pass class TextEditor: def __init__(self) -> None: self._content: str = "" def type(self, text: str) -> None: self._content += text def delete(self, character_count: int) -> str: deleted = self._content[-character_count:] self._content = self._content[:-character_count] return deleted def get_content(self) -> str: return self._content class TypeTextCommand(Command): def __init__(self, editor: TextEditor, text: str) -> None: self._editor = editor self._text = text def execute(self) -> None: self._editor.type(self._text) def undo(self) -> None: self._editor.delete(len(self._text)) class DeleteTextCommand(Command): def __init__(self, editor: TextEditor, character_count: int) -> None: self._editor = editor self._character_count = character_count self._deleted_text: str = "" def execute(self) -> None: self._deleted_text = self._editor.delete(self._character_count) def undo(self) -> None: self._editor.type(self._deleted_text) class EditorHistory: """Invoker — executes commands and manages undo history.""" def __init__(self) -> None: self._history: deque[Command] = deque() def execute(self, command: Command) -> None: command.execute() self._history.append(command) def undo(self) -> None: if self._history: command = self._history.pop() command.undo() # Usage editor = TextEditor() history = EditorHistory() history.execute(TypeTextCommand(editor, "Hello, ")) history.execute(TypeTextCommand(editor, "World!")) print(editor.get_content()) # Hello, World! history.undo() print(editor.get_content()) # Hello, (trailing space — "World!" was deleted) history.execute(TypeTextCommand(editor, "Python!")) print(editor.get_content()) # Hello, Python!
Java:
import java.util.ArrayDeque; import java.util.Deque; // Command interface public interface Command { void execute(); void undo(); } // Receiver public class TextEditor { private StringBuilder content = new StringBuilder(); public void type(String text) { content.append(text); } public String delete(int characterCount) { int start = content.length() - characterCount; String deleted = content.substring(start); content.delete(start, content.length()); return deleted; } public String getContent() { return content.toString(); } } // ConcreteCommand A public class TypeTextCommand implements Command { private final TextEditor editor; private final String text; public TypeTextCommand(TextEditor editor, String text) { this.editor = editor; this.text = text; } @Override public void execute() { editor.type(text); } @Override public void undo() { editor.delete(text.length()); } } // ConcreteCommand B public class DeleteTextCommand implements Command { private final TextEditor editor; private final int characterCount; private String deletedText; public DeleteTextCommand(TextEditor editor, int characterCount) { this.editor = editor; this.characterCount = characterCount; } @Override public void execute() { deletedText = editor.delete(characterCount); } @Override public void undo() { editor.type(deletedText); } } // Invoker public class EditorHistory { private final Deque<Command> history = new ArrayDeque<>(); public void execute(Command command) { command.execute(); history.push(command); } public void undo() { if (!history.isEmpty()) { history.pop().undo(); } } public static void main(String[] args) { TextEditor editor = new TextEditor(); EditorHistory editorHistory = new EditorHistory(); editorHistory.execute(new TypeTextCommand(editor, "Hello, ")); editorHistory.execute(new TypeTextCommand(editor, "World!")); System.out.println(editor.getContent()); // Hello, World! editorHistory.undo(); System.out.println(editor.getContent()); // Hello, (trailing space — "World!" was deleted) editorHistory.execute(new TypeTextCommand(editor, "Java!")); System.out.println(editor.getContent()); // Hello, Java! } }
In the Command pattern, an EditorHistory class holds a stack of Command objects and calls execute() and undo() on them. Which role does EditorHistory fulfill, and what is a key characteristic of that role?
3. Variants & Comparisons
| Variant | How It Works | Pros | Cons | Best For |
|---|---|---|---|---|
| Basic Command | execute() only | Simple, no extra state | No undo | Fire-and-forget actions |
| Undoable Command | execute() + undo() with stored state | Ctrl+Z, history navigation | Every command must save pre-state | Editors, drawing apps, transactions |
| Macro Command | Composite of commands; execute() calls all children | Compose sequences, transactional batches | Undo must reverse in order | Batch operations, wizards |
| Queued Commands | Commands stored in a queue; worker thread drains queue | Async, rate-limiting, scheduling | Command must be serializable if persisted | Job queues, task schedulers |
A drawing application needs to support Ctrl+Z undo functionality, allowing users to reverse their last several actions. Which Command pattern variant is most appropriate, and what key requirement comes with it?
4. When to Use It (and When NOT To)
Use when:
- You need undo/redo (editors, drawing tools, IDEs)
- You need to queue, schedule, or log operations
- You want to parameterize objects with operations (toolbar buttons that are wired to different commands)
- You need transactional behavior (execute a batch; on failure, undo the whole batch)
Don't use when:
- The operation is simple, synchronous, and will never need undo — a direct method call is cleaner
- Commands have no to store and no need for queueing — adds overhead without benefit
Decision triggers:
- "Users need Ctrl+Z" → with
undo() - "I need to schedule these operations for later or retry them on failure" → Command queue
- "I want to log every user action for replay or audit" → Command as log entry
- "I need to support macro recording" → Command
Anti-patterns:
- Fat Commands: Command starts doing business logic that belongs in the Receiver. Command should be a thin dispatcher — business logic lives in the Receiver.
- Missing undo : Implementing
execute()but not saving pre-state meansundo()can't work. Every undoable command must snapshot the state it will change before executing.
A developer is building a text editor and implements a Command pattern for each editing action. They write the execute() method carefully, but skip implementing any state-saving logic because they want to keep the Command classes small. What problem will this cause?
5. Real-World Usage
1. Java Runnable and Callable: Both are single-method interfaces that encapsulate a unit of work — exactly the pattern. ExecutorService.submit(runnable) is an Invoker that queues and executes commands. FutureTask wraps a Callable — deferred execution is a queue of one.
2. UI toolkits (Swing Action, JavaFX): javax.swing.Action is a Command . Menu items, toolbar buttons, and keyboard shortcuts all share the same Action instance — the Invoker wires the trigger, the Command defines the behavior. This decouples "button clicked" from "what happens."
3. Database transaction logs / Write-Ahead Logs (WAL): Each database mutation is recorded as a command in the WAL before being applied. On crash recovery, the log is replayed (execute) or rolled back (undo). The entire concept of ACID undo/redo is built on the Command pattern.
A relational database crashes mid-transaction after several rows have been modified. When the server restarts, it replays its Write-Ahead Log (WAL) to restore consistency. Which role does each recorded WAL entry play in the Command pattern?
6. Interview Cheat Sheet
Key sentences:
- " encapsulates a request as an object — this lets you queue, log, and undo operations without the invoker knowing anything about what's being executed."
- "The critical implementation detail for undo is that each must save the it needs to reverse before calling execute — snapshot first, mutate second."
- "The Invoker is decoupled from the Receiver entirely — it only knows about the Command . This is what lets you wire the same Command to a button, a menu item, and a keyboard shortcut."
- "Macro Commands ( + Command) let you group a sequence of actions into an atomic, undoable unit — useful for transactions and batch operations."
Common follow-up questions:
Q: How do you implement redo in addition to undo?
A: Maintain two stacks — an undo stack and a redo stack. execute() pushes to undo, clears redo. undo() pops from undo, pushes to redo. redo() pops from redo, pushes back to undo.
Q: How is Command different from ? A: encapsulates an algorithm that the Context uses ongoing. Command encapsulates a one-time request that may be queued, logged, or reversed. Strategy is about how something is done; Command is about what was requested and when.
Q: How would you persist commands for replay?
A: Serialize the Command object (with all parameters) to a log (database, file, Kafka topic). On replay, deserialize and call execute() in order. This is the foundation of event sourcing.
Connections:
- Command + → Macro Command (transactional batch)
- Command + → Commands as events; observers react to commands
- Command is the OOP formalization of event sourcing and CQRS command objects
- Command + Memento → store snapshots alongside commands for richer undo
Glossary History
Click dotted jargon to save explanations here.
Glossary History
Click dotted jargon to save explanations here.