Skip to content
FollowtheDamnCode

Command

Turn a request into an object — so it can be queued, logged, retried, and, best of all, undone.

Big idea

Command wraps an action and its arguments in an object with an execute() method (and often undo()). Once a request is an object, you can store it, pass it around, queue it, log it, replay it, or reverse it — none of which you can do with a plain method call that's already happened.

The problem it solves

A spreadsheet has toolbar buttons, keyboard shortcuts, a right-click menu, and a macro recorder, all able to "set cell B2 to 40". If each calls sheet.cells["B2"] = 40 directly:

  • Undo is impossible — nobody remembered the old value.
  • The same action is coded three times in three UI places.
  • Macros and audit logs have nothing to record; the action left no trace.

Think of it like

A restaurant order ticket. The waiter writes your order on a ticket and pins it on the rail; the cook picks it up later. The ticket can wait in a queue, be handed to another cook, be cancelled, and at the end of the night the stack of tickets is a record of everything that happened. The waiter never cooks and the cook never takes orders.

Structure

Invokers hold commands; commands know their receiver
  • Command — the interface (execute, undo).
  • Concrete command (SetCell) — stores the receiver and arguments, and enough to reverse itself.
  • Receiver (Sheet) — the object that does the real work.
  • Invoker (History, a button, a queue) — triggers commands without knowing what they do.

In code

Undo and redo for free once actions are objects
class Sheet:
    def __init__(self):
        self.cells = {}


class SetCell:
    def __init__(self, sheet, cell, value):
        self.sheet, self.cell, self.value = sheet, cell, value
        self.old = None
    def execute(self):
        self.old = self.sheet.cells.get(self.cell)
        self.sheet.cells[self.cell] = self.value
    def undo(self):
        if self.old is None:
            del self.sheet.cells[self.cell]
        else:
            self.sheet.cells[self.cell] = self.old
    def __str__(self):
        return f"set {self.cell}={self.value}"


class History:                                   # the invoker
    def __init__(self):
        self.done, self.undone = [], []
    def run(self, cmd):
        cmd.execute()
        self.done.append(cmd)
        self.undone.clear()                      # a new action ends the redo trail
    def undo(self):
        cmd = self.done.pop(); cmd.undo(); self.undone.append(cmd)
    def redo(self):
        cmd = self.undone.pop(); cmd.execute(); self.done.append(cmd)


sheet, history = Sheet(), History()
history.run(SetCell(sheet, "A1", 10))
history.run(SetCell(sheet, "B2", 40))
history.run(SetCell(sheet, "B2", 45))
print("now:   ", sheet.cells)
history.undo()
print("undo:  ", sheet.cells)
history.undo()
print("undo:  ", sheet.cells)
history.redo()
print("redo:  ", sheet.cells)
print("log:   ", [str(c) for c in history.done])
output
now:    {'A1': 10, 'B2': 45}
undo:   {'A1': 10, 'B2': 40}
undo:   {'A1': 10}
redo:   {'A1': 10, 'B2': 40}
log:    ['set A1=10', 'set B2=40']

Run and edit it in the app.

The button, the shortcut, and the macro recorder can all create a SetCell and hand it to History; none of them knows how undo works.

Predict: what does the sheet hold after this?
class Add:
    def __init__(self, box, n): self.box, self.n = box, n
    def execute(self): self.box.append(self.n)
    def undo(self): self.box.pop()

box, done = [], []
for n in [1, 2, 3]:
    c = Add(box, n); c.execute(); done.append(c)
done.pop().undo()
c = Add(box, 9); c.execute(); done.append(c)
done.pop().undo()
done.pop().undo()
print(box)
What does it print? Show the answer
[1]

Run and edit it in the app.

Watch it

Beyond undo

  • Queues and workers — a job queue is a list of commands; workers call execute().
  • Retries — a failed command object can simply be executed again.
  • Audit and replay — log commands to rebuild state (event sourcing at system scale).
  • Macros — a composite command that executes a list of commands (and undoes them in reverse).

What must a command store to support undo?

When to use it — and when not to

Use it for undo/redo, queued or scheduled work, retries, macros, transactional logs, or to decouple UI controls from the actions they trigger. In Python, a simple callback (lambda or function) is often enough when you only need "do this later" — reach for command objects when you need undo, metadata, or serialisation.

Pros and cons

ProsCons
Undo, redo, queues, logs, retriesA class per action can add up
Invoker decoupled from receiverUndo logic must be written (and tested) per command
Composite commands give macrosCommands holding big state make history heavy

How it relates

  • Memento is the alternative way to undo: snapshot state instead of reversing actions; commands can store mementos.
  • Commands often travel along a Chain of Responsibility.
  • A macro is a Composite of commands.
  • Strategy also wraps behaviour in an object, but chooses how to do something; a command represents a specific request to do it.

Which is a Command rather than a Strategy?

Key terms

  • Command — An object that represents a request, with execute and often undo.
  • Receiver — The object that actually performs the work when a command runs.
  • Invoker — Whatever triggers commands, such as a button, a queue, or a history.
  • Macro — A command made of other commands, run in order and undone in reverse.