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 #
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()
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.