Bird
Raised Fist0
LLDsystem_design~15 mins

Command pattern for undo in LLD - Deep Dive

Choose your learning style10 modes available

Start learning this pattern below

Jump into concepts and practice - no test required

or
Recommended
Test this pattern10 questions across easy, medium, and hard to know if this pattern is strong
Overview - Command pattern for undo
What is it?
The Command pattern for undo is a design approach that lets you record actions as objects. Each action knows how to do itself and how to undo itself. This helps programs reverse changes step-by-step, like pressing Ctrl+Z in a text editor. It separates the action logic from the place where actions are triggered.
Why it matters
Without this pattern, undoing actions would be messy and error-prone because the program would need to remember and reverse every change manually. This pattern makes undo reliable and easy to manage, improving user experience and reducing bugs. It also allows flexible command management, like redoing or batching actions.
Where it fits
Before learning this, you should understand basic object-oriented design and how to encapsulate behavior in objects. After this, you can explore more complex patterns like Memento for state saving or Event Sourcing for full history tracking.
Mental Model
Core Idea
Each user action is wrapped as a command object that knows how to execute and undo itself, enabling easy reversal of operations.
Think of it like...
Imagine writing with a pencil and keeping a list of every stroke you make. If you want to erase the last stroke, you just look at the last item on your list and undo it. Each stroke knows how to erase itself.
┌───────────────┐       ┌───────────────┐       ┌───────────────┐
│   Client      │──────▶│  Command      │──────▶│  Receiver     │
│ (Invoker)     │       │ (Action + Undo)│       │ (Does work)   │
└───────────────┘       └───────────────┘       └───────────────┘
        ▲                      │                        ▲
        │                      │                        │
        │                      ▼                        │
        │               ┌───────────────┐              │
        │               │ Undo Stack    │◀─────────────┘
        │               │ (Stores cmds) │
        │               └───────────────┘
Build-Up - 7 Steps
1
FoundationUnderstanding Commands as Objects
🤔
Concept: Commands encapsulate all information needed to perform an action.
A command object holds the details of an action, like what to do and on which object. For example, a 'DrawLine' command knows how to draw a line on a canvas. This separates the action from the user interface or caller.
Result
You can create, store, and pass around commands independently of how they are executed.
Understanding commands as objects is key because it turns actions into manageable units that can be stored and manipulated.
2
FoundationBasic Undo Concept
🤔
Concept: Each command also knows how to undo itself.
Besides doing the action, commands implement an undo method that reverses the effect. For example, 'DrawLine' can erase the line it drew. This dual behavior is essential for undo functionality.
Result
You can reverse any command by calling its undo method.
Knowing that commands carry their own undo logic simplifies reversing actions without external state tracking.
3
IntermediateManaging Undo with a Stack
🤔Before reading on: do you think undo commands should be stored in a queue or a stack? Commit to your answer.
Concept: Undo commands are stored in a stack to reverse actions in the correct order.
When a command executes, it is pushed onto an undo stack. Undoing means popping the last command and calling its undo method. This LIFO (last-in, first-out) order matches user expectations for undo.
Result
Undo operations happen in reverse order of execution, correctly reverting changes step-by-step.
Understanding the stack structure is crucial because it naturally models the undo sequence users expect.
4
IntermediateDecoupling Invoker and Receiver
🤔Before reading on: do you think the invoker should know how to perform the action or just call the command? Commit to your answer.
Concept: The invoker triggers commands without knowing their details; receivers perform the actual work.
The invoker holds commands and calls execute or undo on them. The receiver is the object that actually changes state, like a document or a drawing canvas. This separation allows flexible command reuse and testing.
Result
Commands can be reused or extended without changing the invoker or receiver.
Decoupling improves modularity and makes the system easier to maintain and extend.
5
IntermediateImplementing Redo Functionality
🤔Before reading on: do you think redo can be done without storing undone commands? Commit to your answer.
Concept: Redo requires a separate stack to store undone commands for re-execution.
When undo is called, the command is popped from the undo stack and pushed onto a redo stack. Redo pops from the redo stack and executes the command again, pushing it back to undo stack. This cycle allows toggling between undo and redo.
Result
Users can move backward and forward through their action history.
Knowing redo needs its own stack prevents common bugs where redo is impossible after undo.
6
AdvancedHandling Complex Commands and Batching
🤔Before reading on: do you think a single command can represent multiple actions? Commit to your answer.
Concept: Commands can be composed to represent multiple actions as one undoable unit.
A composite command groups several commands and executes or undoes them together. This is useful for complex operations like formatting a paragraph or moving multiple objects. It ensures undo treats them as a single step.
Result
Undo and redo work seamlessly for grouped actions, improving user experience.
Understanding command composition allows building flexible and user-friendly undo systems.
7
ExpertOptimizing Undo with State Snapshots
🤔Before reading on: do you think storing full state snapshots is always better than command-based undo? Commit to your answer.
Concept: Sometimes commands store state snapshots to simplify undo, trading memory for simplicity.
Instead of reversing actions, commands can save the state before execution and restore it on undo. This is simpler but uses more memory. Hybrid approaches combine snapshots and command logic for efficiency.
Result
Undo becomes more reliable for complex or non-reversible actions but requires careful memory management.
Knowing when to use snapshots versus pure command undo helps balance performance and complexity in real systems.
Under the Hood
Each command object implements two methods: execute and undo. When execute is called, the command performs its action on the receiver. The command is then pushed onto an undo stack. Undo pops the last command and calls its undo method, reversing the action. Redo uses a separate stack to reapply undone commands. Commands may store necessary state to undo correctly. The invoker only calls execute or undo without knowing command internals.
Why designed this way?
This pattern was designed to separate concerns: the invoker triggers actions, commands encapsulate behavior, and receivers perform work. This separation allows flexible command management, easy undo/redo, and decouples UI from business logic. Alternatives like direct state manipulation were error-prone and hard to maintain. The stack structure matches natural user expectations for undo order.
┌───────────────┐       ┌───────────────┐       ┌───────────────┐
│   Invoker     │──────▶│   Command     │──────▶│   Receiver    │
│ (Client/UI)   │       │ (execute/undo)│       │ (performs act)│
└───────────────┘       └───────────────┘       └───────────────┘
        │                      │                        ▲
        │                      │                        │
        │                      ▼                        │
        │               ┌───────────────┐              │
        │               │ Undo Stack    │◀─────────────┘
        │               └───────────────┘
        │                      │
        │                      ▼
        │               ┌───────────────┐
        │               │ Redo Stack    │
        │               └───────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Does undo always mean reversing the exact last command? Commit to yes or no.
Common Belief:Undo simply reverses the last command exactly as it was executed.
Tap to reveal reality
Reality:Undo may require additional state or logic because some commands are not perfectly reversible by just running the opposite action.
Why it matters:Assuming undo is always a simple reverse leads to bugs where undo fails or corrupts state, especially with complex or external effects.
Quick: Is it okay to store all commands forever for undo? Commit to yes or no.
Common Belief:You can keep every command in memory indefinitely to allow unlimited undo.
Tap to reveal reality
Reality:Storing all commands can consume too much memory; practical systems limit undo history or use snapshots to manage resources.
Why it matters:Ignoring resource limits causes performance issues or crashes in real applications.
Quick: Can redo be done without a separate redo stack? Commit to yes or no.
Common Belief:Redo can be implemented by re-executing commands from the undo stack.
Tap to reveal reality
Reality:Redo requires a separate redo stack because undo removes commands from the undo stack; without it, redo is impossible.
Why it matters:Misunderstanding redo storage leads to broken redo functionality and poor user experience.
Quick: Does the invoker need to know how commands work internally? Commit to yes or no.
Common Belief:The invoker must understand command details to execute or undo them properly.
Tap to reveal reality
Reality:The invoker only calls execute or undo methods; command internals are hidden, enabling loose coupling.
Why it matters:Mixing invoker and command logic reduces modularity and makes maintenance harder.
Expert Zone
1
Commands sometimes need to store pre-execution state snapshots to undo non-reversible actions safely.
2
Composite commands enable grouping multiple actions into a single undoable step, improving user experience.
3
Undo stacks often have size limits and pruning strategies to balance memory use and functionality.
When NOT to use
Avoid using the command pattern for undo when actions are simple and stateless, or when full state snapshots (Memento pattern) are more efficient. For distributed systems, event sourcing or version control may be better alternatives.
Production Patterns
In real systems, undo commands are often serialized for persistence, allowing undo after restarts. Systems use command queues with transactional guarantees to ensure consistency. UI frameworks integrate command pattern with keyboard shortcuts and menus for seamless user control.
Connections
Memento pattern
Complementary pattern
Understanding command pattern helps grasp how Memento stores object states for undo, showing two ways to implement undo functionality.
Event sourcing
Builds on command history
Event sourcing records all changes as events (commands), enabling full history replay and undo, extending the command pattern to distributed systems.
Legal contract revocation
Similar concept in law
Undoing a command is like legally revoking a contract: both require a clear record of the original action and a defined way to reverse it, showing cross-domain parallels.
Common Pitfalls
#1Not storing enough state to undo complex commands
Wrong approach:class DeleteTextCommand { execute() { document.deleteText(); } undo() { document.insertText(); } // no stored text }
Correct approach:class DeleteTextCommand { constructor(text) { this.text = text; } execute() { document.deleteText(); } undo() { document.insertText(this.text); } }
Root cause:Assuming undo can recreate state without saving necessary data leads to incomplete or broken undo.
#2Using a queue instead of a stack for undo storage
Wrong approach:undoQueue.enqueue(command); command = undoQueue.dequeue(); command.undo();
Correct approach:undoStack.push(command); command = undoStack.pop(); command.undo();
Root cause:Misunderstanding undo order causes commands to be undone in the wrong sequence.
#3Clearing redo stack incorrectly after new command
Wrong approach:undoStack.push(newCommand); // forgot to clear redoStack
Correct approach:undoStack.push(newCommand); redoStack.clear();
Root cause:Not clearing redo stack after new actions breaks redo history and confuses users.
Key Takeaways
The command pattern encapsulates actions and their undo logic into objects, enabling flexible undo and redo.
Undo operations use a stack to reverse actions in the correct order, matching user expectations.
Decoupling the invoker, command, and receiver improves modularity and maintainability.
Redo requires a separate stack to store undone commands for re-execution.
Advanced undo systems use composite commands and state snapshots to handle complex scenarios efficiently.

Practice

(1/5)
1. What is the main purpose of the Command pattern in the context of undo functionality?
easy
A. To replace all conditional statements with loops
B. To directly modify the user interface without storing history
C. To store data in a database for permanent record
D. To encapsulate actions as objects with execute and undo methods

Solution

  1. Step 1: Understand the role of Command pattern

    The Command pattern wraps actions as objects, allowing them to be executed and undone independently.
  2. Step 2: Relate to undo functionality

    This wrapping enables storing commands in a history stack, so undo can call the undo method on the last command.
  3. Final Answer:

    To encapsulate actions as objects with execute and undo methods -> Option D
  4. Quick Check:

    Command pattern = encapsulate actions for undo [OK]
Hint: Command pattern wraps actions for undo/redo [OK]
Common Mistakes:
  • Thinking Command pattern modifies UI directly
  • Confusing Command pattern with data storage
  • Assuming it replaces loops or conditionals
2. Which method signature correctly belongs to a Command interface supporting undo?
easy
A. void save(); void load();
B. void execute(); void undo();
C. void start(); void finish();
D. void run(); void stop();

Solution

  1. Step 1: Identify standard Command interface methods

    The Command pattern typically defines an execute() method to perform the action and an undo() method to reverse it.
  2. Step 2: Match method signatures

    Only void execute(); void undo(); has execute() and undo(), matching the Command pattern for undo.
  3. Final Answer:

    void execute(); void undo(); -> Option B
  4. Quick Check:

    Command methods = execute and undo [OK]
Hint: Look for execute() and undo() methods [OK]
Common Mistakes:
  • Choosing unrelated method names like run/stop
  • Confusing start/finish with undo functionality
  • Assuming save/load are Command methods
3. Given the following code snippet, what will be the output after calling undo() on the last command?
class AddCommand:
    def __init__(self, value, receiver):
        self.value = value
        self.receiver = receiver
    def execute(self):
        self.receiver.total += self.value
    def undo(self):
        self.receiver.total -= self.value

class Receiver:
    def __init__(self):
        self.total = 0

receiver = Receiver()
cmd1 = AddCommand(5, receiver)
cmd2 = AddCommand(3, receiver)
cmd1.execute()
cmd2.execute()
cmd2.undo()
print(receiver.total)
medium
A. 5
B. 8
C. 3
D. 0

Solution

  1. Step 1: Trace command executions

    Initially, receiver.total = 0. After cmd1.execute(), total = 0 + 5 = 5. After cmd2.execute(), total = 5 + 3 = 8.
  2. Step 2: Apply undo on cmd2

    cmd2.undo() subtracts 3, so total = 8 - 3 = 5.
  3. Final Answer:

    5 -> Option A
  4. Quick Check:

    Execute adds, undo subtracts = 5 [OK]
Hint: Undo reverses last execute effect on total [OK]
Common Mistakes:
  • Forgetting to subtract on undo
  • Assuming undo resets total to zero
  • Mixing order of execute and undo
4. Identify the bug in this undo implementation of a Command pattern:
class MultiplyCommand:
    def __init__(self, value, receiver):
        self.value = value
        self.receiver = receiver
        self.prev = None
    def execute(self):
        self.prev = self.receiver.total
        self.receiver.total *= self.value
    def undo(self):
        self.receiver.total /= self.value

receiver = type('Receiver', (), {'total': 10})()
cmd = MultiplyCommand(2, receiver)
cmd.execute()
cmd.undo()
print(receiver.total)
medium
A. Execute should add instead of multiply
B. Undo method is missing
C. Undo should restore previous value, not divide
D. Receiver class is not defined

Solution

  1. Step 1: Analyze execute and undo methods

    Execute saves previous total and multiplies current total by value. Undo divides total by value.
  2. Step 2: Identify problem with undo

    Undo divides by value, but if value is zero or changed, this may not restore original total exactly. It should restore saved previous total instead.
  3. Final Answer:

    Undo should restore previous value, not divide -> Option C
  4. Quick Check:

    Undo must restore saved state, not recalculate [OK]
Hint: Undo must restore saved state, not recalculate [OK]
Common Mistakes:
  • Assuming division always reverses multiplication
  • Not saving previous state before execute
  • Ignoring edge cases like zero multiplication
5. You are designing a text editor with undo using the Command pattern. Which approach best supports multiple undo and redo operations efficiently?
hard
A. Use two stacks: one for undo commands, one for redo commands
B. Store all commands in a single list without pointers
C. Only keep the last command for undo, discard others
D. Save full document snapshots after each command

Solution

  1. Step 1: Understand undo/redo requirements

    Undo reverses last command, redo reapplies commands undone. Efficient support requires tracking both undo and redo history.
  2. Step 2: Evaluate data structures

    Two stacks allow pushing commands on execute, popping for undo, and pushing undone commands to redo stack. This supports multiple undo/redo efficiently.
  3. Final Answer:

    Use two stacks: one for undo commands, one for redo commands -> Option A
  4. Quick Check:

    Two stacks = efficient undo/redo [OK]
Hint: Two stacks handle undo and redo efficiently [OK]
Common Mistakes:
  • Using single list without tracking position
  • Keeping only last command loses history
  • Saving full snapshots wastes memory