Building Undo and Redo Features With the Command Pattern
Undo and redo are among the most requested features in user-facing applications. Implementing them correctly requires careful design. The Command Pattern provides a clean, extensible architecture for undo/redo functionality.
Key sources: "Design Patterns" by Gamma, Helm, Johnson, Vlissides (Gang of Four), Martin Fowler's Patterns of Enterprise Application Architecture.
The Problem
A user edits a document. They delete a paragraph, change a font size, insert an image, and then realize they want the paragraph back. The application must reverse the delete operation without affecting the subsequent changes.
Naive approaches fail:
- Storing snapshots of the entire state after every operation consumes too much memory.
- Manually coding undo logic for each operation leads to duplicated, error-prone code.
- Using the system clipboard or undo manager provided by the platform limits portability.
The Command Pattern solves this by encapsulating each operation as an object with both an execute and an undo method.
The Command Pattern
The Command Pattern turns a request into a standalone object. This object contains all the information needed to perform the operation and, critically, to reverse it.
from abc import ABC, abstractmethod
class Command(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
Each concrete command implements both methods:
class DeleteTextCommand(Command):
def __init__(self, document, start, end):
self.document = document
self.start = start
self.end = end
self.deleted_text = ""
def execute(self):
self.deleted_text = self.document.get_text(self.start, self.end)
self.document.delete(self.start, self.end)
def undo(self):
self.document.insert(self.start, self.deleted_text)
The Undo Manager
An undo manager maintains a stack of executed commands. When the user performs undo, it pops the top command and calls its undo method. When the user performs redo, it pushes the command back onto the undo stack and calls execute again.
class UndoManager:
def __init__(self):
self.undo_stack = []
self.redo_stack = []
def execute(self, command):
command.execute()
self.undo_stack.append(command)
self.redo_stack.clear() # New action invalidates redo history
def undo(self):
if not self.undo_stack:
return
command = self.undo_stack.pop()
command.undo()
self.redo_stack.append(command)
def redo(self):
if not self.redo_stack:
return
command = self.redo_stack.pop()
command.execute()
self.undo_stack.append(command)
This design handles the core undo/redo logic. Each command is executed once, stored, and can be reversed by calling undo. Redo reverses the undo by executing the command again.
Composite Commands
Some operations are composed of multiple sub-operations. For example, "paste text" might involve inserting text, updating formatting, and adjusting cursor position. These should be undone as a single unit.
class CompositeCommand(Command):
def __init__(self):
self.commands = []
def add(self, command):
self.commands.append(command)
def execute(self):
for cmd in self.commands:
cmd.execute()
def undo(self):
# Reverse order
for cmd in reversed(self.commands):
cmd.undo()
Managing Memory
Storing every command in memory can consume significant resources. Strategies for managing the undo stack:
| Strategy | Behavior | Best For | |----------|----------|----------| | Fixed stack size | Discard oldest commands when the stack reaches a limit | Memory-constrained environments | | Snapshot + commands | Periodically store a full state snapshot. Commands before the snapshot are discarded. | Long-running sessions | | Coalescing | Merge consecutive commands of the same type (e.g., 20 character insertions merge into one "type text" command) | Text editors, drawing apps | | Command pooling | Reuse command objects instead of allocating new ones | High-frequency operations |
Real-World Examples
Text Editors
Every text editor uses a variant of the Command Pattern. Vim's . command repeats the last change — it is storing the last command and replaying it. Emacs's undo-tree tracks branching undo history rather than a linear stack.
Photoshop
Photoshop implements undo as a history panel showing the last 50-100 operations. Each operation can be reverted to any point in history. This is a stack with random access — users can jump to any previous state, not just step backward linearly.
Git
Git does not use the Command Pattern directly, but its model is conceptually similar. Each commit is a snapshot. git revert creates a new commit that undoes a previous commit. git reset moves the branch pointer backward. The key difference: Git stores full snapshots rather than incremental changes.
Limitations
The Command Pattern has limitations:
- Memory for fine-grained operations: Each character typed becomes a separate command. Coalescing mitigates this.
- Non-reversible operations: Sending an email, writing to a socket, or launching a missile cannot be cleanly undone. These operations should not use the Command Pattern for undo.
- State-dependent undo: If a command depends on the application state at the time of execution, undoing after other commands have changed the state can produce incorrect results.
Key Takeaways
- The Command Pattern encapsulates operations as objects with execute and undo methods.
- An undo manager maintains a stack of executed commands for undo and redo.
- Composite Commands group multiple operations into a single undoable unit.
- Coalescing, fixed stack sizes, and periodic snapshots manage memory usage.
- Not all operations are reversible — design commands with undo in mind from the start.
Design principle: If an operation can be undone, encapsulate it as a Command object. This separates the undo logic from the business logic and makes the system extensible.