Design PatternsIntermediate 12 min Lesson 11 of 13

Command — Actions as Objects

Turn a request into an object so it can be queued, logged, retried or undone, instead of being a method call that vanishes.

Design Patterns · Lesson 11 of 13
0/13 done(0%)

What is it? #

The Command pattern wraps a request in an object holding everything needed to perform it: what to do, and with what data.

A normal method call happens and is gone. A command object can be stored, sent somewhere else, executed later, recorded in a log, retried after failure, or reversed.

That last point is the most visible use. If a command knows how to undo itself, a stack of executed commands gives you undo and redo almost for free.

The cost is ceremony. A class per action is more code than a function call, so it is worth it only when you need one of those capabilities.

Think of it like this #

A restaurant order slip. The waiter writes it rather than shouting to the kitchen. The slip can be queued behind others, passed to whoever is free, checked later against the bill, and voided if the customer changes their mind.

The spoken version cannot be queued, audited or cancelled.

Simple example #

A document editor needs undo and redo. A background system needs jobs that can be queued and retried. Both are the same pattern: the action becomes an object.

Code #

PYTHON
from abc import ABC, abstractmethod


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


class Document:
    def __init__(self): self.text = ""


class AppendText(Command):
    def __init__(self, document: Document, text: str):
        self.document, self.text = document, text

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

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


class ReplaceAll(Command):
    def __init__(self, document: Document, old: str, new: str):
        self.document, self.old, self.new = document, old, new
        self._previous = None

    def execute(self):
        self._previous = self.document.text                 # remember, to undo
        self.document.text = self.document.text.replace(self.old, self.new)

    def undo(self):
        if self._previous is not None:
            self.document.text = self._previous


class History:
    def __init__(self):
        self._done: list[Command] = []
        self._undone: list[Command] = []

    def run(self, command: Command):
        command.execute()
        self._done.append(command)
        self._undone.clear()            # a new action clears the redo branch

    def undo(self):
        if self._done:
            command = self._done.pop()
            command.undo()
            self._undone.append(command)

    def redo(self):
        if self._undone:
            command = self._undone.pop()
            command.execute()
            self._done.append(command)


doc = Document()
history = History()
history.run(AppendText(doc, "Hello "))
history.run(AppendText(doc, "world"))
history.run(ReplaceAll(doc, "world", "there"))
print(doc.text)        # Hello there
history.undo()
print(doc.text)        # Hello world
history.redo()
print(doc.text)        # Hello there


# The same idea as a queued job
class SendInvoiceEmail(Command):
    def __init__(self, order_id, email):
        self.order_id, self.email = order_id, email

    def execute(self):
        print(f"emailing invoice for {self.order_id} to {self.email}")

    def undo(self):
        raise NotImplementedError("an email cannot be unsent")


queue: list[Command] = [SendInvoiceEmail("A-1", "[email protected]")]
while queue:
    queue.pop(0).execute()
TEXT
When to use it
  - undo and redo are required
  - actions need queuing, scheduling or retrying
  - you need an audit trail of what was done and with what data

When NOT to use it
  - a direct method call is enough
  - the action cannot meaningfully be undone or deferred
  - you would end up with a class per trivial operation

How it works #

Each command stores its target and its arguments. AppendText knows the document and the text, which is everything needed to run it or reverse it.

AppendText.undo computes the reversal from what it knows. ReplaceAll cannot do that — a replace is lossy — so it saves the previous state during execute. Choosing between "compute the inverse" and "snapshot the state" is the main design decision when adding undo.

History keeps two stacks. run executes and records; undo pops from done, reverses, and pushes to undone; redo does the reverse.

Clearing the redo stack on a new action matches what every editor does: once you type something after undoing, the previous redo branch is gone.

The email command shows the honest limit. Some actions cannot be undone, and pretending otherwise is worse than admitting it. In a queue context, undo may not be part of the interface at all.

The queue example is the same objects used differently: because the action is data, it can be stored, serialised and executed elsewhere. That is exactly how background job systems work.

Real-world use #

Text editors, drawing tools and spreadsheets use command stacks for undo. Database transaction logs are the same idea, which is what makes rollback and replication possible.

Background job systems serialise commands into a queue: the job name plus its arguments, executed later by a worker, retried on failure. Celery tasks and similar systems are commands in all but name.

In event-sourced systems, commands express intent and the resulting events are stored, giving a complete audit trail and the ability to rebuild state by replaying.

Keyboard shortcuts and menu actions in desktop applications map to command objects, which is why the same action can be bound to a button, a shortcut and a script.

Common mistakes #

  • Writing a command class for every trivial call, drowning in boilerplate.
  • Forgetting to capture the state needed for undo before executing.
  • Assuming every action is reversible — some genuinely are not.
  • Keeping unlimited history and consuming memory; cap the stack.
  • Putting business logic in the command instead of delegating to a service.

Practice #

Implement commands for a simple shopping cart: add item, remove item and apply discount, each with undo. Run a sequence, undo twice, redo once, and print the cart at each step. Then mark one command as non-undoable and handle that cleanly in the history.

Quick quiz

  1. 1. What does the Command pattern turn a request into?

  2. 2. Why does `ReplaceAll` store the previous text?

  3. 3. Why clear the redo stack when a new command runs?

  4. 4. How do background job systems relate to this pattern?

  5. 5. When is this pattern too much?

Summary

  • A command object holds an action and everything it needs to run.
  • Actions as data can be queued, logged, retried and undone.
  • Undo either computes the inverse or restores a saved snapshot.
  • Some actions cannot be undone — model that honestly.
  • Background job systems and transaction logs are this pattern at scale.