Design Patterns, Explained Simply · Behavioral Patterns: How Objects Share Work

Command

Turn a request into an object, so you can queue it, log it, retry it and, most usefully, undo it.

The problem

A text editor's toolbar buttons call editor methods directly: the "Bold" button calls editor.bold(), the "Delete" button calls editor.delete(). Now users want undo and redo, a macro feature that replays a sequence of actions, and keyboard shortcuts that do the same thing as buttons.

With direct calls, there's no record of what happened, so nothing to undo or replay. Each button and shortcut duplicates the logic.

Real-world analogy

A restaurant order slip. The waiter doesn't cook; they write the order on a slip (the command) and put it in a queue. The kitchen executes slips in order. The slip can be cancelled, kept as a record, or repeated ("same again").

Without the pattern

def on_type_clicked(text):
    editor.content += text         # no record of what happened, so nothing to undo
def on_delete_clicked(n):
    editor.content = editor.content[:-n]

The pattern

  • Command interface: execute() and, for undo, undo().
  • Concrete commands (TypeText, DeleteText): store everything needed to perform the action and to reverse it.
  • Receiver (Document): the object that does the real work.
  • Invoker (Editor with a history): runs commands and keeps them in a history stack for undo/redo, without knowing what each command does.

The code

Undo and redo with command objectsTry it yourself
class Document:  # receiver
    def __init__(self):
        self.text = ""


class TypeText:
    def __init__(self, doc, text):
        self.doc, self.text = doc, text

    def execute(self):
        self.doc.text += self.text

    def undo(self):
        self.doc.text = self.doc.text[: len(self.doc.text) - len(self.text)]


class DeleteLast:
    def __init__(self, doc, count):
        self.doc, self.count, self.removed = doc, count, ""

    def execute(self):
        self.removed = self.doc.text[-self.count:]  # remember what to restore
        self.doc.text = self.doc.text[: -self.count]

    def undo(self):
        self.doc.text += self.removed


class Editor:  # invoker: runs commands and keeps the history
    def __init__(self):
        self.done, self.undone = [], []

    def run(self, command):
        command.execute()
        self.done.append(command)
        self.undone.clear()

    def undo(self):
        command = self.done.pop()
        command.undo()
        self.undone.append(command)

    def redo(self):
        command = self.undone.pop()
        command.execute()
        self.done.append(command)


doc, editor = Document(), Editor()
editor.run(TypeText(doc, "Hello"))
editor.run(TypeText(doc, ", world"))
editor.run(DeleteLast(doc, 5))
print(f"after edits: '{doc.text}'")
editor.undo()
print(f"undo:        '{doc.text}'")
editor.undo()
print(f"undo:        '{doc.text}'")
editor.redo()
print(f"redo:        '{doc.text}'")

Output

after edits: 'Hello, '
undo:        'Hello, world'
undo:        'Hello'
redo:        'Hello, world'

The editor's history knows nothing about typing or deleting; it just calls execute() and undo(). A new action (bold, paste) is a new command class.

When to use it

  • Undo/redo, history and macros.
  • Queues and background jobs: a job is a command object serialised to a queue and executed later by a worker.
  • Retries and audit logs: commands can be stored, replayed or logged.
  • Decoupling UI elements (buttons, menus, shortcuts) from the operations they trigger.

When not to use it

  • A simple callback is enough and you don't need undo, queuing or history. In Python and TypeScript a function or lambda is a lightweight command.
  • Undo is hard for actions with external side effects (sending an email, charging a card). Those need compensating actions (refund, follow-up email), not a simple undo().

You've already used it

  • Job queues: Laravel jobs, Celery tasks, Sidekiq and BullMQ jobs are serialisable command objects.
  • Redux: actions are command-like objects describing what happened; reducers apply them.
  • CQRS: the "C" stands for commands, such as PlaceOrder objects handled by command handlers.
  • Java: Runnable and Callable passed to an executor.

Key points

  • Command turns a request into an object with execute() (and often undo()).
  • Commands can be queued, logged, retried, replayed and undone.
  • The invoker runs commands without knowing what they do.
  • Side effects in the outside world need compensating actions, not a naive undo.
  • Further reading: Refactoring Guru: Command.

Practice questions

How does the Command pattern make undo and redo possible?

Answer

Each action becomes an object that stores what it needs to execute and reverse itself. An invoker keeps executed commands on a history stack; undo pops the last command and calls undo(), redo re-executes it.

How do background job queues relate to the Command pattern?

Answer

A queued job is a command object: it holds the data needed to perform an action and has a handle()/execute() method. It is serialised to a queue and executed later by a worker, possibly retried. Laravel jobs and Celery tasks are examples.

Why is undo difficult for commands with external side effects?

Answer

You can't "un-send" an email or simply reverse a card charge. Such commands need compensating actions (a refund, a correction email) instead of restoring previous state.

Open in the app to mark lessons complete, take notes and track your progress.