Command Pattern

advanced 4-7 YOE lld design-pattern behavioral

The Command pattern turns a request into a standalone object. This object contains everything about the request — what to do, who does it, and any parameters needed. Once it’s an object, we can pass it around, queue it, log it, and even undo it.

Think of it like a restaurant order slip. We tell the waiter “I want a burger.” The waiter writes it on a slip (the command object) and hands it to the kitchen. The kitchen executes it later. The waiter doesn’t cook. The kitchen doesn’t take orders. And if we change our mind, we can cancel the slip.

The Problem

Imagine we’re building a text editor. We need undo/redo, keyboard shortcuts, menu actions, and toolbar buttons — all triggering the same operations. Without Command:

  • The UI directly calls business logic (tight coupling)
  • Undo/redo is nearly impossible without tracking every change
  • We can’t queue operations or replay them
  • Adding a new action means changing multiple places

Key Components

Command Pattern Structure
Invoker
Triggers the command.
Button, menu item,
keyboard shortcut.
─▶
Command
execute()
undo()
Wraps the request
as an object.
─▶
Receiver
Does the actual work.
TextDocument,
Light, Thermostat.
Invoker doesn't know what the command does. Command doesn't know who triggered it.
  • Command — interface with execute() and optionally undo()
  • Concrete Command — implements the command, holds a reference to the receiver
  • Invoker — triggers the command (doesn’t know what it does)
  • Receiver — the object that actually performs the work

Implementation — Text Editor with Undo/Redo

from abc import ABC, abstractmethod

class Command(ABC):
    @abstractmethod
    def execute(self) -> None:
        pass

    @abstractmethod
    def undo(self) -> None:
        pass

class TextDocument:
    """The Receiver -- does the actual work."""
    def __init__(self):
        self.content = ""

    def insert(self, text: str, position: int):
        self.content = self.content[:position] + text + self.content[position:]

    def delete(self, position: int, length: int) -> str:
        deleted = self.content[position:position + length]
        self.content = self.content[:position] + self.content[position + length:]
        return deleted

class InsertCommand(Command):
    def __init__(self, document: TextDocument, text: str, position: int):
        self.document = document
        self.text = text
        self.position = position

    def execute(self):
        self.document.insert(self.text, self.position)

    def undo(self):
        self.document.delete(self.position, len(self.text))

class DeleteCommand(Command):
    def __init__(self, document: TextDocument, position: int, length: int):
        self.document = document
        self.position = position
        self.length = length
        self.deleted_text = ""

    def execute(self):
        self.deleted_text = self.document.delete(self.position, self.length)

    def undo(self):
        self.document.insert(self.deleted_text, self.position)

class Editor:
    """The Invoker -- triggers commands and manages history."""
    def __init__(self, document: TextDocument):
        self.document = document
        self.history: list[Command] = []
        self.redo_stack: list[Command] = []

    def execute(self, command: Command):
        command.execute()
        self.history.append(command)
        self.redo_stack.clear()

    def undo(self):
        if not self.history:
            return
        cmd = self.history.pop()
        cmd.undo()
        self.redo_stack.append(cmd)

    def redo(self):
        if not self.redo_stack:
            return
        cmd = self.redo_stack.pop()
        cmd.execute()
        self.history.append(cmd)

# Usage
doc = TextDocument()
editor = Editor(doc)

editor.execute(InsertCommand(doc, "Hello", 0))
print(doc.content)  # "Hello"

editor.execute(InsertCommand(doc, " World", 5))
print(doc.content)  # "Hello World"

editor.undo()
print(doc.content)  # "Hello"

editor.redo()
print(doc.content)  # "Hello World"
class TextDocument {
  constructor() {
    this.content = "";
  }

  insert(text, position) {
    this.content = this.content.slice(0, position) + text + this.content.slice(position);
  }

  delete(position, length) {
    const deleted = this.content.slice(position, position + length);
    this.content = this.content.slice(0, position) + this.content.slice(position + length);
    return deleted;
  }
}

class InsertCommand {
  constructor(document, text, position) {
    this.document = document;
    this.text = text;
    this.position = position;
  }

  execute() {
    this.document.insert(this.text, this.position);
  }

  undo() {
    this.document.delete(this.position, this.text.length);
  }
}

class DeleteCommand {
  constructor(document, position, length) {
    this.document = document;
    this.position = position;
    this.length = length;
    this.deletedText = "";
  }

  execute() {
    this.deletedText = this.document.delete(this.position, this.length);
  }

  undo() {
    this.document.insert(this.deletedText, this.position);
  }
}

class Editor {
  constructor(document) {
    this.document = document;
    this.history = [];
    this.redoStack = [];
  }

  execute(command) {
    command.execute();
    this.history.push(command);
    this.redoStack = [];
  }

  undo() {
    if (this.history.length === 0) return;
    const cmd = this.history.pop();
    cmd.undo();
    this.redoStack.push(cmd);
  }

  redo() {
    if (this.redoStack.length === 0) return;
    const cmd = this.redoStack.pop();
    cmd.execute();
    this.history.push(cmd);
  }
}

// Usage
const doc = new TextDocument();
const editor = new Editor(doc);

editor.execute(new InsertCommand(doc, "Hello", 0));
console.log(doc.content); // "Hello"

editor.execute(new InsertCommand(doc, " World", 5));
console.log(doc.content); // "Hello World"

editor.undo();
console.log(doc.content); // "Hello"
import java.util.Stack;

interface Command {
    void execute();
    void undo();
}

class TextDocument {
    StringBuilder content = new StringBuilder();

    void insert(String text, int position) {
        content.insert(position, text);
    }

    String delete(int position, int length) {
        String deleted = content.substring(position, position + length);
        content.delete(position, position + length);
        return deleted;
    }

    String getContent() { return content.toString(); }
}

class InsertCommand implements Command {
    private TextDocument document;
    private String text;
    private int position;

    InsertCommand(TextDocument doc, String text, int position) {
        this.document = doc;
        this.text = text;
        this.position = position;
    }

    public void execute() { document.insert(text, position); }
    public void undo() { document.delete(position, text.length()); }
}

class DeleteCommand implements Command {
    private TextDocument document;
    private int position, length;
    private String deletedText = "";

    DeleteCommand(TextDocument doc, int position, int length) {
        this.document = doc;
        this.position = position;
        this.length = length;
    }

    public void execute() { deletedText = document.delete(position, length); }
    public void undo() { document.insert(deletedText, position); }
}

class Editor {
    private TextDocument document;
    private Stack<Command> history = new Stack<>();
    private Stack<Command> redoStack = new Stack<>();

    Editor(TextDocument document) { this.document = document; }

    void execute(Command cmd) {
        cmd.execute();
        history.push(cmd);
        redoStack.clear();
    }

    void undo() {
        if (history.isEmpty()) return;
        Command cmd = history.pop();
        cmd.undo();
        redoStack.push(cmd);
    }

    void redo() {
        if (redoStack.isEmpty()) return;
        Command cmd = redoStack.pop();
        cmd.execute();
        history.push(cmd);
    }
}

When to Use

  • Undo/Redo — text editors, drawing apps, any operation we need to reverse
  • Task queues — schedule commands to run later, retry on failure
  • Macro recording — record a sequence of commands and replay them
  • Transaction management — execute a batch, roll back everything if one fails
  • Remote control / smart home — “turn on lights” is a command object

When NOT to Use

  • Simple direct calls that will never need undo, queuing, or logging
  • When the command just wraps a single method call with no extra behavior — that’s pointless indirection
  • When we don’t need any of the benefits (undo, queue, log) — it’s just extra classes

Common Interview Questions

Q: How does Command enable macro recording? Store a list of executed commands. To replay the macro, just loop through the list and call execute() on each one. Simple as that.

Q: How do we implement transactions with Command? Execute commands in sequence. If any fails, call undo() on all previously executed commands in reverse order. Same idea as database rollback.

In simple language, Command is like writing down a task on a sticky note instead of doing it immediately. Once it’s written down, we can stick it on a board (queue), throw it away (cancel), or unstick it and reverse what it did (undo). The sticky note IS the command.