Command Pattern

7 min read

Reading Progress0%
Object-Oriented Design Index
Tier 1 -- Foundations
Tier 2
Tier 3
Tier 4
Tier 5
Object-Oriented Design Index
Tier 1 -- Foundations
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.


QUICK CHECK

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?

Choose one answer

2. How It Works

Participants:

  • with execute() and optionally undo()
  • ConcreteCommand — binds a specific action to a specific Receiver; implements execute() by calling receiver methods; stores enough to implement undo()
  • 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:

  1. Define with execute() (and undo() if reversibility is needed)
  2. Implement ConcreteCommand — stores a reference to its Receiver and any parameters needed; execute() delegates to Receiver
  3. Invoker holds a command (or a queue); calls execute() without knowing the details
  4. 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!
    }
}

QUICK CHECK

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?

Choose one answer

3. Variants & Comparisons

VariantHow It WorksProsConsBest For
Basic Commandexecute() onlySimple, no extra stateNo undoFire-and-forget actions
Undoable Commandexecute() + undo() with stored stateCtrl+Z, history navigationEvery command must save pre-stateEditors, drawing apps, transactions
Macro CommandComposite of commands; execute() calls all childrenCompose sequences, transactional batchesUndo must reverse in orderBatch operations, wizards
Queued CommandsCommands stored in a queue; worker thread drains queueAsync, rate-limiting, schedulingCommand must be serializable if persistedJob queues, task schedulers

QUICK CHECK

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?

Choose one answer

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 means undo() can't work. Every undoable command must snapshot the state it will change before executing.

QUICK CHECK

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?

Choose one answer

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.


QUICK CHECK

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?

Choose one answer

6. Interview Cheat Sheet

Key sentences:

  1. " encapsulates a request as an object — this lets you queue, log, and undo operations without the invoker knowing anything about what's being executed."
  2. "The critical implementation detail for undo is that each must save the it needs to reverse before calling execute — snapshot first, mutate second."
  3. "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."
  4. "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.