Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Design Patterns (GoF and modern)  Module 4 – Behavioral patterns: algorithms and requests

Command: turn requests into objects

Wrap each action as an object with execute and undo, then build undo and redo stacks, macros and a command log that is replayed after a restart.

  • Intermediate
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, Java 25, Go 1.27.1 and Rust 1.99.0
  • By MySmartCoPilot
Free core, Pro depth: 10 of 30 minutes free
Free: The problem: actions that leave no trace · The pattern: invoker, command and receiver · Undo and redo with two stacks · 3 more sections. With a pass: 2 in-depth parts and 3 interview answers, about 20 minutes.

What you will learn

  • Wrap actions as command objects with execute and undo
  • Build undo and redo stacks, macro commands and a command queue
  • Represent commands as closures or enums where that is simpler
  • Design commands that are safe to retry and replay by giving them identities

Before you start

Command is a design pattern that turns a request into an object. The object holds everything needed to carry the request out later: what to do, what to do it to and with which values. The code that runs it needs to know nothing about any of that, only how to say “execute”. Once a request is an object it can be kept, and keeping it is the point: in a history, so it can be undone; in a queue, so a worker can run it later; in a log, so it can be replayed after a crash; in a list, so several requests run as one macro. The Gang of Four catalogue gives exactly those uses: parameterising clients with requests, queueing and logging them, and supporting undo.

Reach for Command when an action must outlive the moment it was asked for: undo and redo, background jobs, retries, an audit trail of who did what.

The problem: actions that leave no trace

An editor’s toolbar that calls buffer.insert(pos, text) directly works until the first request for undo. The call changed the text and then vanished: nothing remembers what was inserted, where, or what a delete removed. Adding undo to that design means teaching the buffer about every edit any button can make, and adding a new button means changing the buffer again. Queueing work for later or retrying it after a failure hits the same wall, because there is no object to put in the queue.

The pattern: invoker, command and receiver

Command splits the action into four roles. The command interface has execute() and, for undo, undo(). Each concrete command, such as an insert or a delete, stores its arguments and a reference to the receiver, the object that does the real work (here the text buffer). The invoker runs commands and keeps them; here it is a history with two stacks. The client builds commands and hands them to the invoker:

Class diagram: History keeps Commands on two stacks; Insert, Delete and Macro realise Command; Insert and Delete edit a TextBuffer.History (invoker)-doneCommand[*]-undoneCommand[*]+run(command)None+undo()None+redo()None«interface»Command+execute()None+undo()NoneTextBuffer (receiver)+textstr+insert(pos, s)None+delete(pos, n)strInsert-posint-sstr+execute()None+undo()NoneDelete-posint-nint-removedstr+execute()None+undo()NoneMacro-commandsCommand[*]+execute()None+undo()Nonekeeps 0..*on two stacksruns and undoesits stepseditsedits

Command in the editor example: the invoker, the command interface, three commands and the receiver

Text description of the diagram

Six class boxes. Each box shows the class name, then its attributes, then its operations; a plus sign marks a public member and a minus sign a private one, and the type follows each name.

At the top is History, labelled the invoker. It has two private lists of commands, done and undone, and three public operations: run(command), undo() and redo(). A line with a hollow diamond at History and an arrowhead at Command, labelled "keeps 0..* on two stacks", says that a history holds any number of commands.

Below it is Command, marked «interface», with two operations: execute() and undo().

Three classes in the bottom row realise Command, each joined to it by a dashed line with a hollow triangle at Command. Macro has a private list of commands; its line is labelled "runs and undoes its steps". Delete has a private pos, n and removed, the text it took out, which it needs for undo. Insert has a private pos and s, the text it adds.

To the right of Command is TextBuffer, labelled the receiver, with a public attribute text and operations insert(pos, s) and delete(pos, n), which returns the deleted text. Solid arrows labelled "edits" run from Insert and from Delete to TextBuffer: those commands do their work by calling the buffer.

The buffer knows nothing about undo, and the history knows nothing about text. A new kind of edit is a new command class; neither of them changes.

Undo and redo with two stacks

The history runs each command and pushes it onto an undo stack. Undo pops the last command, calls its undo() and pushes it onto a redo stack; redo moves it back the other way. A macro is a command made of commands, which executes its steps in order and undoes them in reverse:

A text buffer with undo, redo and a macro Python · editor/editor.py
# A text buffer edited through commands. Each command keeps what it needs to run and to undo itself; the history
# (the invoker) runs commands and keeps them on two stacks, one to undo and one to redo.


class TextBuffer:  # the receiver: it does the work and knows nothing about commands
    def __init__(self, text=""):
        self.text = text

    def insert(self, pos, s):
        self.text = self.text[:pos] + s + self.text[pos:]

    def delete(self, pos, n):
        removed = self.text[pos:pos + n]
        self.text = self.text[:pos] + self.text[pos + n:]
        return removed


class Insert:
    def __init__(self, buffer, pos, s):
        self.buffer, self.pos, self.s = buffer, pos, s

    def execute(self):
        self.buffer.insert(self.pos, self.s)

    def undo(self):
        self.buffer.delete(self.pos, len(self.s))

    def __str__(self):
        return f"insert {self.s!r} at {self.pos}"


class Delete:
    def __init__(self, buffer, pos, n):
        self.buffer, self.pos, self.n = buffer, pos, n
        self.removed = ""

    def execute(self):
        self.removed = self.buffer.delete(self.pos, self.n)  # kept for undo

    def undo(self):
        self.buffer.insert(self.pos, self.removed)

    def __str__(self):
        return f"delete {self.n} at {self.pos}"


class Macro:
    """Several commands that run, and undo, as one."""

    def __init__(self, name, commands):
        self.name, self.commands = name, commands

    def execute(self):
        for c in self.commands:
            c.execute()

    def undo(self):
        for c in reversed(self.commands):
            c.undo()

    def __str__(self):
        return f"macro {self.name!r} ({len(self.commands)} steps)"


class History:  # the invoker: runs commands and remembers them
    def __init__(self):
        self.done, self.undone = [], []

    def run(self, command):
        command.execute()
        self.done.append(command)
        self.undone.clear()  # a new command makes the old redo steps meaningless

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

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


buf = TextBuffer("Meet at 10")
h = History()


def show(what):
    print(f"{what:<34} {buf.text!r:<34} undo {len(h.done)}  redo {len(h.undone)}")


show("start")
for command in [Insert(buf, 10, " tomorrow"), Delete(buf, 8, 2), Insert(buf, 8, "11")]:
    h.run(command)
    show(str(command))
h.run(Macro("sign", [Insert(buf, len(buf.text), "."), Insert(buf, len(buf.text) + 1, " -- Asha")]))
show(str(h.done[-1]))
show(f"undo {h.undo()}")
show(f"undo {h.undo()}")
show(f"redo {h.redo()}")
h.run(Insert(buf, 0, "Team: "))
show("insert 'Team: ' at 0")

Output

start                              'Meet at 10'                       undo 0  redo 0
insert ' tomorrow' at 10           'Meet at 10 tomorrow'              undo 1  redo 0
delete 2 at 8                      'Meet at  tomorrow'                undo 2  redo 0
insert '11' at 8                   'Meet at 11 tomorrow'              undo 3  redo 0
macro 'sign' (2 steps)             'Meet at 11 tomorrow. -- Asha'     undo 4  redo 0
undo macro 'sign' (2 steps)        'Meet at 11 tomorrow'              undo 3  redo 1
undo insert '11' at 8              'Meet at  tomorrow'                undo 2  redo 2
redo insert '11' at 8              'Meet at 11 tomorrow'              undo 3  redo 1
insert 'Team: ' at 0               'Team: Meet at 11 tomorrow'        undo 4  redo 0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 editor.py

Each command keeps exactly what its undo needs. An insert can be undone from its position and the length of its text. A delete cannot: it has to remember the text it removed, which it only knows once it has run, so it saves it in execute(). The macro sign came back with a single undo, and the last line shows the rule that keeps redo honest: running a new command empties the redo stack, because the undone edits were made for a text that no longer exists.

Choices every command design makes

The example settles the roles; real systems make five more decisions, and each has a name:

  • Inverse commands undo by applying the opposite edit, keeping only the data that edit needs.
  • Snapshots, the Memento pattern, undo by restoring a saved copy of the whole state taken before the command.
  • Atomic macros undo the steps that already ran when a later step fails, so a macro happens fully or not at all.
  • Closures and enums replace command classes: a closure is the lightest command, and an enum is a command that can be printed and parsed.
  • Command logs record every command before it runs, so replaying the log rebuilds the state after a restart.
  • Command IDs let a receiver recognise a retried command and apply it once.

Practise: undo and redo for a spreadsheet

Exercise · Medium · Python

Undo and redo for a spreadsheet cell editor

sheet.py edits spreadsheet cells directly, so nothing can be taken back. Give it undo and redo by running every edit as a command.

  • set(cell, value), clear(cell) and bulk_set({cell: value, …}) each become one command that remembers what it needs to undo itself: the value each cell had before, or that it had none.
  • undo() undoes the last edit that has not been undone and returns True; with nothing to undo it returns False and changes nothing. redo() does the same in the other direction.
  • bulk_set is one step: one undo() takes back all of its cells, and one redo() puts them back.
  • A new edit after an undo empties the redo stack: the undone edits can no longer be redone.

The sample tests check five sequences: a set that is undone and redone, two sets on one cell, a clear, a bulk set, and a new edit after an undo.

Starter code · sheet.py

class Sheet:
    """Spreadsheet cells by name ("A1", "B2"), edited through set, clear and bulk_set, with undo and redo.

    undo() takes back the last edit that has not been undone and returns True, or returns False when there is
    nothing to undo. redo() puts back the last edit that was undone and returns True, or returns False when there is
    nothing to redo. bulk_set() is one edit: one undo takes all of its cells back. Any new edit after an undo means
    the undone edits can no longer be redone.
    """

    def __init__(self):
        self.cells = {}

    def get(self, cell):
        return self.cells.get(cell)

    def set(self, cell, value):
        self.cells[cell] = value

    def clear(self, cell):
        self.cells.pop(cell, None)

    def bulk_set(self, values):
        for cell, value in values.items():
            self.cells[cell] = value

    def undo(self):
        # Replace this with your code: run every edit as a command that knows how to undo itself.
        return False

    def redo(self):
        # Replace this with your code.
        return False
The sample tests · test_sheet.py
from sheet import Sheet


def test_undo_and_redo_a_set():
    """undoes a set, then redoes it"""
    s = Sheet()
    s.set("A1", 10)
    assert s.undo() is True
    assert s.get("A1") is None
    assert s.redo() is True
    assert s.get("A1") == 10


def test_undo_brings_back_the_previous_value():
    """restores the value a cell had before it was overwritten"""
    s = Sheet()
    s.set("A1", 1)
    s.set("A1", 2)
    s.undo()
    assert s.get("A1") == 1
    s.undo()
    assert s.get("A1") is None
    assert s.undo() is False


def test_undo_a_clear():
    """puts a cleared value back"""
    s = Sheet()
    s.set("B2", "total")
    s.clear("B2")
    assert s.get("B2") is None
    s.undo()
    assert s.get("B2") == "total"


def test_bulk_set_is_one_step():
    """takes back every cell of a bulk_set with one undo, and puts them back with one redo"""
    s = Sheet()
    s.set("A1", 5)
    s.bulk_set({"A1": 1, "B1": 2, "C1": 3})
    s.undo()
    assert [s.get(c) for c in ("A1", "B1", "C1")] == [5, None, None]
    s.redo()
    assert [s.get(c) for c in ("A1", "B1", "C1")] == [1, 2, 3]


def test_a_new_edit_clears_redo():
    """refuses to redo after a new edit"""
    s = Sheet()
    s.set("A1", 1)
    s.undo()
    s.set("B1", 2)
    assert s.redo() is False
    assert s.get("A1") is None
    assert s.get("B1") == 2
A hint

One command class is enough: an edit that gives some cells new values, where None clears a cell. In execute(), first save each affected cell's current value (or a marker meaning "this cell was empty"), then apply the change; undo() puts the saved values back. set, clear and bulk_set then only build that command and hand it to one method that executes it, pushes it on the undo stack and empties the redo stack.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

Check yourself

3 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 3 A user undoes two edits and then types a new character. What should happen to the two undone edits?

    Choose one answer.

    Show the answer to question 1

    Answer: The redo stack is emptied, because they were made for a text that no longer exists

    A new command starts a new branch of history. The undone edits record positions in the old text, so redoing them after the text changed would edit the wrong place. Running any new command empties the redo stack.

  2. Question 2 of 3 What is wrong with this delete command's undo?

    Read the code, then choose one answer.

    class Delete:
        def __init__(self, buf, pos, n):
            self.buf, self.pos, self.n = buf, pos, n
    
        def execute(self):
            self.buf.delete(self.pos, self.n)
    
        def undo(self):
            self.buf.insert(self.pos, " " * self.n)
    Show the answer to question 2

    Answer: It cannot put back what was deleted, because execute() never saved the removed text

    An insert can be undone from its own arguments, but a delete cannot: the text it removes is only known when it runs. execute() must keep the return value of buf.delete(), and undo() inserts that text back at pos.

  3. Question 3 of 3 A macro ran its steps in the order A, B, C. Put the calls its undo makes in order.

    Give each item its position, from 1 (first).

    Show the answer to question 3

    Answer:

    1. C.undo()
    2. B.undo()
    3. A.undo()

    Each step was made for the text the previous step left, so they are undone in reverse: C first, which returns the text to the state B produced, then B, then A.

These questions check the roles and the two-stack rule.

Undo that holds up: inverse commands, snapshots and macros, tested Pro: opens with a Pro, Premium or Ultimate pass

Undo is where command designs fail in practice. Here the two ways to undo are measured against each other, macros are made all-or-nothing, and a seeded replay of 200 edits catches a bug that the usual undo test misses.

In this part:

  • Inverse commands or snapshots: what each keeps, measured
  • The redo stack after a new command: a stale redo reproduced
  • A seeded replay that tests the redo rule
  • Macros that change everything or nothing

4 runnable examples (Python, commands.py) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Commands as data: closures, enums, logs and retries Pro: opens with a Pro, Premium or Ultimate pass

A command does not have to be a class, and a command that is plain data can be written down, sent and replayed. This part compares the lightweight forms in the track’s languages and shows where each one stops working.

In this part:

  • Closures and partial: the lightest commands, and their loop trap
  • The same trap in Go, JavaScript and Java
  • Enums and tagged records that can be logged and replayed
  • Command IDs: a retry that changes nothing

4 runnable examples (Go, Java, Java: counting loop, JavaScript, Python) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    How is the Command pattern different from the Strategy pattern?

    A strategy is a way of doing a job: the context calls it to work something out, such as a shipping cost, and keeps using it. A command is a job to be done: an object that carries one request with its arguments, such as "insert this text here", so that it can run later, wait in a queue, be logged, retried or undone. Strategies are usually chosen once from configuration and hold no data about a single call; a new command is made for every request and holds that request's data. Both can be written as functions in most languages, so the difference is the intent: how, against what.

  2. Mid to senior

    An image editor offers 500 steps of undo on 50 MB images. A snapshot of the whole image after every step would need about 25 GB. How would you design undo so that it fits in memory, and what does each choice cost?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    A worker runs ChargeCard commands from a queue. It crashed after the card was charged but before it acknowledged the message, so the queue delivered the command again. How do you make sure the customer is charged once?

    Model answer, Pro. First step and follow-ups

  4. Mid

    A macro command applies five edits to a document, and the fourth one fails. What state should the document end in, and how do you implement that? What changes when the edits go to a server one by one?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Command turns a request into an object that holds what to do, to what and with which values, so it can be kept and run later.
  • The invoker runs and keeps commands; the receiver does the work; neither knows about the other.
  • Two stacks give undo and redo, and every new command empties the redo stack.
  • A command keeps what its undo needs; a delete saves the text it removed when it runs.
  • A macro is a command made of commands: steps run in order and are undone in reverse.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.