bigonotes
CS ConceptsDesign Patterns

Command Pattern

Turn requests into objects so you can queue, log, combine and undo them, and decouple buttons from the work they trigger.

6 min readLast updated Design Patterns

A button that calls light.turn_on() directly can only ever turn on that light. You also can't queue the action, log it, or undo it.

Command wraps a request in an object with an execute() method. The button just runs whatever command it's given, and because commands are objects, you can store them in a history, put them in a queue, or combine them into macros.

Intent#

Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations. (GoF)

Analogy: a restaurant order slip. You don't shout at the chef; you hand the waiter a written order. The slip can be queued, passed along, or cancelled, and the waiter doesn't need to know how to cook.

The problem: invoker welded to receiver#

Here the button calls the light directly:

class Button:
    def __init__(self, light: Light):
        self.light = light

    def click(self) -> None:
        self.light.turn_on()  # this button can only ever do this one thing
  • Button knows about Light, so you can't reuse it for other actions.
  • There's nowhere to hook in queuing, logging or undo.

With Command, the button works with any action:

button.set_command(LightOnCommand(light))
button.set_command(DoorOpenCommand(door))
button.set_command(PlayMusicCommand(stereo))

Structure#

Command has five participants:

Participant Role Example
Command Interface with execute() (and often undo()) Command
Concrete command Holds a receiver and the parameters; execute() calls the receiver LightOnCommand
Receiver The object that actually does the work Light
Invoker Stores commands and triggers them, without knowing what they do Switch, RemoteControl
Client Creates commands and wires them to invokers main

A single button press flows through them like this:

  1. Client creates the command: LightOnCommand(light).
  2. Invoker stores it: switch.set_command(light_on).
  3. User triggers the invoker: switch.press().
  4. Invoker calls command.execute().
  5. Command calls the receiver: light.turn_on().

Example 1: light switch#

The switch holds one command and runs it on every press:

from abc import ABC, abstractmethod


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


# Receiver
class Light:
    def turn_on(self) -> None:
        print("Light: ON")

    def turn_off(self) -> None:
        print("Light: OFF")


# Concrete commands
class LightOnCommand(Command):
    def __init__(self, light: Light):
        self.light = light

    def execute(self) -> None:
        self.light.turn_on()


class LightOffCommand(Command):
    def __init__(self, light: Light):
        self.light = light

    def execute(self) -> None:
        self.light.turn_off()


# Invoker
class Switch:
    def __init__(self) -> None:
        self.command: Command | None = None

    def set_command(self, command: Command) -> None:
        self.command = command

    def press(self) -> None:
        self.command.execute()  # no idea what it does


light = Light()
wall_switch = Switch()
wall_switch.set_command(LightOnCommand(light))
wall_switch.press()  # Light: ON
wall_switch.set_command(LightOffCommand(light))
wall_switch.press()  # Light: OFF

Example 2: a programmable remote#

The invoker holds slots of commands; any device can be bound to any slot.

class RemoteControl:
    def __init__(self, slots: int = 3) -> None:
        self.on_commands: list[Command | None] = [None] * slots
        self.off_commands: list[Command | None] = [None] * slots

    def set_command(self, slot: int, on: Command, off: Command) -> None:
        self.on_commands[slot] = on
        self.off_commands[slot] = off

    def press_on(self, slot: int) -> None:
        self.on_commands[slot].execute()

    def press_off(self, slot: int) -> None:
        self.off_commands[slot].execute()


remote = RemoteControl()
remote.set_command(0, TVOnCommand(tv), TVOffCommand(tv))
remote.set_command(1, FanOnCommand(fan), FanOffCommand(fan))
remote.press_on(0)   # TV: ON
remote.press_on(1)   # Fan: ON
remote.press_off(0)  # TV: OFF

Example 3: text editor with undo and redo#

Each command knows how to reverse itself. The invoker keeps the history.

from abc import ABC, abstractmethod


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

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


# Receiver
class TextEditor:
    def __init__(self) -> None:
        self._text = ""

    def write(self, words: str) -> None:
        self._text += words

    def delete_last(self, length: int) -> None:
        self._text = self._text[:max(0, len(self._text) - length)]

    def get_text(self) -> str:
        return self._text


class WriteCommand(Command):
    def __init__(self, editor: TextEditor, text: str):
        self.editor = editor
        # The command remembers what it needs to undo itself
        self.text = text

    def execute(self) -> None:
        self.editor.write(self.text)

    def undo(self) -> None:
        self.editor.delete_last(len(self.text))


# Invoker with history (lists used as stacks)
class EditorInvoker:
    def __init__(self) -> None:
        self.undo_stack: list[Command] = []
        self.redo_stack: list[Command] = []

    def run(self, c: Command) -> None:
        c.execute()
        self.undo_stack.append(c)
        self.redo_stack.clear()  # a new action invalidates redo history

    def undo(self) -> None:
        if not self.undo_stack:
            return
        c = self.undo_stack.pop()
        c.undo()
        self.redo_stack.append(c)

    def redo(self) -> None:
        if not self.redo_stack:
            return
        c = self.redo_stack.pop()
        c.execute()
        self.undo_stack.append(c)


editor = TextEditor()
invoker = EditorInvoker()
invoker.run(WriteCommand(editor, "Hello "))
invoker.run(WriteCommand(editor, "World!"))
invoker.undo()  # "Hello "
invoker.redo()  # "Hello World!"
print(editor.get_text())

Example 4: queued orders#

Commands can be collected now and executed later, which is the basis of job queues.

from abc import ABC, abstractmethod


class Chef:  # receiver
    def cook(self, dish: str) -> None:
        print(f"Chef: cooking {dish}")


class Order(ABC):  # command
    @abstractmethod
    def execute(self) -> None: ...


class DishOrder(Order):
    def __init__(self, chef: Chef, dish: str):
        self.chef, self.dish = chef, dish

    def execute(self) -> None:
        self.chef.cook(self.dish)


class Waiter:  # invoker: queues commands, runs them later
    def __init__(self) -> None:
        self._orders: list[Order] = []

    def take_order(self, order: Order) -> None:
        self._orders.append(order)

    def send_to_kitchen(self) -> None:
        for order in self._orders:
            order.execute()
        self._orders.clear()


chef, waiter = Chef(), Waiter()
waiter.take_order(DishOrder(chef, "burger"))
waiter.take_order(DishOrder(chef, "pasta"))
waiter.send_to_kitchen()
# Chef: cooking burger
# Chef: cooking pasta

Example 5: macro command#

A macro is a command made of commands (a Composite of commands).

class MacroCommand(Command):
    def __init__(self, commands: list[Command]):
        self.commands = commands

    def execute(self) -> None:
        for c in self.commands:
            c.execute()


party_mode = MacroCommand([
    LightOnCommand(light),
    TVOnCommand(tv),
    StereoOnCommand(stereo),
])
party_mode.execute()  # Light: ON, TV: ON, Stereo: ON

Template#

This is the minimal shape of the pattern:

from abc import ABC, abstractmethod


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


class Receiver:
    def action(self) -> None:
        print("Receiver: performing action")


class ConcreteCommand(Command):
    def __init__(self, receiver: Receiver):
        self.receiver = receiver

    def execute(self) -> None:
        self.receiver.action()


class Invoker:
    def __init__(self) -> None:
        self.command: Command | None = None

    def set_command(self, command: Command) -> None:
        self.command = command

    def invoke(self) -> None:
        self.command.execute()

Real-world examples#

Commands appear wherever work is packaged up to run later or be reversed:

  • Callables submitted to a concurrent.futures executor are commands: work packaged as objects to run later on another thread.
  • GUI action objects (such as Qt's QAction): one action shared by a menu item, a toolbar button and a keyboard shortcut.
  • Undo/redo in editors and IDEs; each edit is a command.
  • Job schedulers and queues (Celery tasks, cron-style schedulers, message-queue workers).
  • Transactions and workflows: deposits and withdrawals as commands that can be logged, replayed or compensated.
  • Games: player moves as commands, which also makes replays easy.

In languages with first-class functions, a simple command is often just a function or lambda (button.on_click(lambda: light.turn_on())). You need a full class when the command must carry undo(), metadata or serializable state.

When to use it#

Command pays off when you need any of these:

  • You need undo/redo.
  • You want to queue, schedule or retry operations, or run them on another thread.
  • You need an audit log of operations.
  • You want macros: combining several operations into one.
  • The invoker (UI, scheduler) should be decoupled from the receiver.

When not to use it#

Skip it when:

  • A direct method call does the job and you need none of queuing, logging or undo.
  • The operation is one-off and never parameterized or reused.

Pitfalls#

Watch for these issues:

  • Class explosion: one class per action. Use lambdas for simple commands.
  • Undo is harder than it looks. The command must capture enough state to reverse itself. When reversing is hard, store a snapshot (Memento) instead.
  • Memory. Long undo histories hold every command; cap the history size.
  • Clear the redo stack when a new command runs, or redo will replay stale actions.

Command vs similar patterns#

Command is often confused with these patterns:

Pattern Difference
Strategy Encapsulates how to do a task (an algorithm); Command encapsulates what to do (a request). "Save file" is a command; "which compression to use" is a strategy
Observer Notifies many listeners of an event; Command is a one-way request to one receiver
Chain of Responsibility Passes a request along until someone handles it; a command already knows its receiver
Memento Stores state for restore; Command stores an operation. They're often combined for undo

Key takeaways#

  • Command turns a request into an object with execute() (and optionally undo()).
  • The invoker triggers commands without knowing what they do or who the receiver is.
  • Because commands are objects, you can queue, log, combine and undo them.
  • Undo/redo = two stacks of commands; clear redo on every new command.
  • For simple cases, a lambda or function is a perfectly good command.

Found a mistake? Tell me on LinkedIn ↗

Further reading#