Bird
Raised Fist0
Interview Prepoop-design-patternshardAmazonGoogleFlipkartCREDRazorpay

Command Pattern - Undo/Redo, Request Queuing & Logging

Choose your preparation mode3 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
🎯
Command Pattern - Undo/Redo, Request Queuing & Logging
hardOOPAmazonGoogleFlipkart

Imagine a text editor where every user action can be undone or redone, commands can be queued for batch execution, and all operations are logged for audit. How do you design such a system cleanly and flexibly?

💡 This problem involves designing a system using the Command Pattern, which encapsulates requests as objects. Beginners often struggle because it requires understanding how to decouple the invoker of an operation from the object that performs it, and how to manage undo/redo stacks and command queues effectively. Think of it like a remote control that can remember your button presses and undo or redo them on demand.
📋
Problem Statement

Design and implement the Command Pattern to support the following features: - Encapsulate requests as command objects. - Support undo and redo operations. - Allow queuing of commands for batch execution. - Maintain a log of executed commands. Input: A sequence of commands to execute, undo, redo, or queue. Output: The state changes resulting from command execution and the ability to undo/redo commands as requested.

Commands can be any operation on a receiver object.Undo/redo operations must restore the receiver to previous states.Queued commands execute in order.Logging must record command execution details.
💡
Example
Input"Execute CommandA, Execute CommandB, Undo, Redo, Queue CommandC and CommandD, Execute Queue"
OutputCommandA executed, CommandB executed, CommandB undone, CommandB redone, CommandC executed, CommandD executed

Commands are executed in order; undo reverses last command; redo reapplies it; queued commands execute in batch.

  • Undo when no commands have been executed → no operation
  • Redo when no commands have been undone → no operation
  • Execute empty command queue → no operation
  • Undo multiple commands in sequence → state restored stepwise
⚠️
Common Mistakes
Not clearing redo stack after new command execution

Redo stack contains invalid commands leading to incorrect redo behavior

Clear redo stack whenever a new command is executed

Undoing commands in the wrong order in MacroCommand

State becomes inconsistent because commands are undone in forward order

Undo commands in reverse order to maintain correct state restoration

Not handling empty undo or redo stacks before popping

Runtime errors or crashes due to popping from empty stack

Check if stacks are empty before undo or redo operations

Executing commands directly without encapsulation

Tight coupling between invoker and receiver, no undo/redo possible

Encapsulate operations as Command objects with execute and undo methods

Not logging command lifecycle events

No audit trail, making debugging and tracking difficult

Add a logger to record enqueue, execute, undo, and redo events

🧠
Brute Force (Direct Command Execution with Manual Undo/Redo Stacks)
💡 Starting with a straightforward implementation helps understand the core responsibilities of the Command Pattern and the challenges of managing undo/redo manually. It's like learning to ride a bike with training wheels before trying tricks.

Intuition

Encapsulate each operation as a command object with execute and undo methods. Maintain two stacks: one for undo and one for redo. Execute commands directly and push them onto the undo stack. Undo pops from undo stack and pushes onto redo stack, and vice versa.

Algorithm

  1. Define a Command interface with execute() and undo() methods.
  2. Implement concrete commands for each operation on the receiver.
  3. Maintain two stacks: undoStack and redoStack.
  4. On execute, call command.execute(), push to undoStack, and clear redoStack.
  5. On undo, pop from undoStack, call undo(), push to redoStack.
  6. On redo, pop from redoStack, call execute(), push to undoStack.
💡 The challenge is to carefully manage the stacks to ensure undo and redo behave correctly and consistently. Think of it like managing a history of actions where you can go back and forth without losing track.
</>
Code
from abc import ABC, abstractmethod

class Command(ABC):
    @abstractmethod
    def execute(self):
        pass

    @abstractmethod
    def undo(self):
        pass

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

    def action(self, value):
        self.state += value
        print(f"Receiver state changed to {self.state}")

    def revert(self, value):
        self.state -= value
        print(f"Receiver state reverted to {self.state}")

class AddCommand(Command):
    def __init__(self, receiver, value):
        self.receiver = receiver
        self.value = value

    def execute(self):
        self.receiver.action(self.value)

    def undo(self):
        self.receiver.revert(self.value)

class Invoker:
    def __init__(self):
        self.undo_stack = []
        self.redo_stack = []

    def execute_command(self, command):
        command.execute()
        self.undo_stack.append(command)
        self.redo_stack.clear()

    def undo(self):
        if not self.undo_stack:
            print("Nothing to undo")
            return
        command = self.undo_stack.pop()
        command.undo()
        self.redo_stack.append(command)

    def redo(self):
        if not self.redo_stack:
            print("Nothing to redo")
            return
        command = self.redo_stack.pop()
        command.execute()
        self.undo_stack.append(command)

# Driver code
if __name__ == "__main__":
    receiver = Receiver()
    invoker = Invoker()

    cmd1 = AddCommand(receiver, 10)
    cmd2 = AddCommand(receiver, 20)

    invoker.execute_command(cmd1)  # Receiver state changed to 10
    invoker.execute_command(cmd2)  # Receiver state changed to 30
    invoker.undo()                 # Receiver state reverted to 10
    invoker.redo()                 # Receiver state changed to 30
Line Notes
class Command(ABC):Defines the Command interface to enforce execute and undo methods, ensuring all commands follow the same contract.
def execute(self):Abstract method to perform the command's action, allowing polymorphic execution.
def undo(self):Abstract method to reverse the command's action, enabling undo functionality.
self.undo_stack.append(command)Push executed command to undo stack for future undo operations.
self.redo_stack.clear()Clear redo stack because new command invalidates redo history, maintaining correctness.
if not self.undo_stack:Check to prevent undo when no commands have been executed, avoiding errors.
command.undo()Undo the last executed command to revert state changes.
self.redo_stack.append(command)Store undone command for possible redo, preserving history.
import java.util.Stack;

interface Command {
    void execute();
    void undo();
}

class Receiver {
    private int state = 0;

    public void action(int value) {
        state += value;
        System.out.println("Receiver state changed to " + state);
    }

    public void revert(int value) {
        state -= value;
        System.out.println("Receiver state reverted to " + state);
    }
}

class AddCommand implements Command {
    private Receiver receiver;
    private int value;

    public AddCommand(Receiver receiver, int value) {
        this.receiver = receiver;
        this.value = value;
    }

    public void execute() {
        receiver.action(value);
    }

    public void undo() {
        receiver.revert(value);
    }
}

class Invoker {
    private Stack<Command> undoStack = new Stack<>();
    private Stack<Command> redoStack = new Stack<>();

    public void executeCommand(Command command) {
        command.execute();
        undoStack.push(command);
        redoStack.clear();
    }

    public void undo() {
        if (undoStack.isEmpty()) {
            System.out.println("Nothing to undo");
            return;
        }
        Command command = undoStack.pop();
        command.undo();
        redoStack.push(command);
    }

    public void redo() {
        if (redoStack.isEmpty()) {
            System.out.println("Nothing to redo");
            return;
        }
        Command command = redoStack.pop();
        command.execute();
        undoStack.push(command);
    }
}

public class Main {
    public static void main(String[] args) {
        Receiver receiver = new Receiver();
        Invoker invoker = new Invoker();

        Command cmd1 = new AddCommand(receiver, 10);
        Command cmd2 = new AddCommand(receiver, 20);

        invoker.executeCommand(cmd1); // Receiver state changed to 10
        invoker.executeCommand(cmd2); // Receiver state changed to 30
        invoker.undo();               // Receiver state reverted to 10
        invoker.redo();               // Receiver state changed to 30
    }
}
Line Notes
interface Command {Defines the Command interface with execute and undo methods to standardize command behavior.
private Stack<Command> undoStack = new Stack<>();Stack to keep track of executed commands for undo operations.
undoStack.push(command);Push command after execution to enable undo functionality.
redoStack.clear();Clear redo stack on new command execution to maintain correct redo history.
if (undoStack.isEmpty()) {Check to avoid undo when no commands have been executed, preventing errors.
command.undo();Undo the last executed command to revert the receiver's state.
redoStack.push(command);Store undone command for possible redo operations.
command.execute();Redo command execution to reapply the command.
#include <iostream>
#include <stack>

class Command {
public:
    virtual void execute() = 0;
    virtual void undo() = 0;
    virtual ~Command() {}
};

class Receiver {
    int state = 0;
public:
    void action(int value) {
        state += value;
        std::cout << "Receiver state changed to " << state << std::endl;
    }
    void revert(int value) {
        state -= value;
        std::cout << "Receiver state reverted to " << state << std::endl;
    }
};

class AddCommand : public Command {
    Receiver* receiver;
    int value;
public:
    AddCommand(Receiver* r, int v) : receiver(r), value(v) {}
    void execute() override {
        receiver->action(value);
    }
    void undo() override {
        receiver->revert(value);
    }
};

class Invoker {
    std::stack<Command*> undoStack;
    std::stack<Command*> redoStack;
public:
    void executeCommand(Command* command) {
        command->execute();
        undoStack.push(command);
        while (!redoStack.empty()) redoStack.pop();
    }
    void undo() {
        if (undoStack.empty()) {
            std::cout << "Nothing to undo" << std::endl;
            return;
        }
        Command* command = undoStack.top();
        undoStack.pop();
        command->undo();
        redoStack.push(command);
    }
    void redo() {
        if (redoStack.empty()) {
            std::cout << "Nothing to redo" << std::endl;
            return;
        }
        Command* command = redoStack.top();
        redoStack.pop();
        command->execute();
        undoStack.push(command);
    }
};

int main() {
    Receiver receiver;
    Invoker invoker;

    AddCommand cmd1(&receiver, 10);
    AddCommand cmd2(&receiver, 20);

    invoker.executeCommand(&cmd1); // Receiver state changed to 10
    invoker.executeCommand(&cmd2); // Receiver state changed to 30
    invoker.undo();                // Receiver state reverted to 10
    invoker.redo();                // Receiver state changed to 30

    return 0;
}
Line Notes
class Command {Abstract base class defining execute and undo interface to enforce command contract.
std::stack<Command*> undoStack;Stack to track executed commands for undo operations.
undoStack.push(command);Push command after execution to enable undo functionality.
while (!redoStack.empty()) redoStack.pop();Clear redo stack on new command execution to maintain redo correctness.
if (undoStack.empty()) {Prevent undo when no commands have been executed, avoiding runtime errors.
command->undo();Undo last executed command to revert receiver state.
redoStack.push(command);Store undone command for possible redo.
command->execute();Redo command execution to reapply the command.
class Command {
    execute() {
        throw new Error('execute() must be implemented');
    }
    undo() {
        throw new Error('undo() must be implemented');
    }
}

class Receiver {
    constructor() {
        this.state = 0;
    }
    action(value) {
        this.state += value;
        console.log(`Receiver state changed to ${this.state}`);
    }
    revert(value) {
        this.state -= value;
        console.log(`Receiver state reverted to ${this.state}`);
    }
}

class AddCommand extends Command {
    constructor(receiver, value) {
        super();
        this.receiver = receiver;
        this.value = value;
    }
    execute() {
        this.receiver.action(this.value);
    }
    undo() {
        this.receiver.revert(this.value);
    }
}

class Invoker {
    constructor() {
        this.undoStack = [];
        this.redoStack = [];
    }
    executeCommand(command) {
        command.execute();
        this.undoStack.push(command);
        this.redoStack = [];
    }
    undo() {
        if (this.undoStack.length === 0) {
            console.log('Nothing to undo');
            return;
        }
        const command = this.undoStack.pop();
        command.undo();
        this.redoStack.push(command);
    }
    redo() {
        if (this.redoStack.length === 0) {
            console.log('Nothing to redo');
            return;
        }
        const command = this.redoStack.pop();
        command.execute();
        this.undoStack.push(command);
    }
}

// Driver code
const receiver = new Receiver();
const invoker = new Invoker();

const cmd1 = new AddCommand(receiver, 10);
const cmd2 = new AddCommand(receiver, 20);

invoker.executeCommand(cmd1); // Receiver state changed to 10
invoker.executeCommand(cmd2); // Receiver state changed to 30
invoker.undo();               // Receiver state reverted to 10
invoker.redo();               // Receiver state changed to 30
Line Notes
class Command {Defines abstract Command interface with execute and undo to enforce consistent command behavior.
this.undoStack.push(command);Push executed command to undo stack for future undo operations.
this.redoStack = [];Clear redo stack on new command execution to maintain redo correctness.
if (this.undoStack.length === 0) {Check to avoid undo when no commands have been executed, preventing errors.
command.undo();Undo last executed command to revert receiver state.
this.redoStack.push(command);Store undone command for possible redo operations.
command.execute();Redo command execution to reapply the command.
Complexity
TimeO(1) per command execution, undo, or redo
SpaceO(n) for storing executed commands in stacks

Each command executes or undoes in constant time; stacks grow linearly with number of commands executed, so space is proportional to command history size.

💡 For 100 commands, expect about 100 operations stored; undo/redo operations are very fast because they just pop or push from stacks.
Interview Verdict: Accepted

This approach is simple and correct but does not support advanced features like command queuing or logging yet.

📊
All Approaches - One-Glance Tradeoffs
💡 In interviews, coding the enhanced Command Pattern with undo/redo stacks and basic queuing is usually sufficient. Macro commands are advanced and useful for transactional operations.
ApproachTimeSpaceStack RiskReconstructUse In Interview
1. Brute ForceO(1) per commandO(n) for undo/redo stacksNoYesIntroduce basic Command Pattern and undo/redo
2. Enhanced with Queuing and LoggingO(1) per operationO(n) for stacks, queue, and logsNoYesCode this for real-world scenarios with batch execution and audit
3. Macro Command (Transactional)O(m) per macro commandO(n + m) for stacks and macro storageNoYesMention or code if asked about transactional undo/redo
💼
Interview Strategy
💡 Use this guide to understand the problem deeply before interviews. Start by clarifying requirements, then explain the brute force approach, and progressively improve your design. Practice coding and testing each approach to build confidence.

How to Present

Step 1: Clarify requirements - undo/redo, queuing, logging, transactional needs.Step 2: Describe the basic Command Pattern with execute and undo methods.Step 3: Explain undo/redo stacks and how they manage command history.Step 4: Introduce command queuing and logging for batch execution and audit.Step 5: Discuss macro commands for transactional grouping and atomic undo/redo.Step 6: Code the chosen approach and test with edge cases.

Time Allocation

Clarify: 5min → Approach explanation: 10min → Coding: 20min → Testing & discussion: 10min. Total ~45min

What the Interviewer Tests

Understanding of design patterns, ability to manage command history, handling edge cases in undo/redo, and extending the pattern for queuing and transactional operations.

Common Follow-ups

  • How would you implement redo functionality? → Use a redo stack to store undone commands.
  • How to support batch execution of commands? → Use a command queue and execute commands in order.
  • How to log all command executions? → Add a logger that records each command's lifecycle events.
  • How to group multiple commands as one undoable action? → Implement a MacroCommand that executes and undoes commands as a group.
💡 These follow-ups test your ability to extend the basic pattern to real-world complexities and demonstrate deeper design skills.
🔍
Pattern Recognition

When to Use

1) Need to decouple request sender from receiver 2) Support undo/redo operations 3) Batch or queue commands for later execution 4) Log or audit command executions

Signature Phrases

'undo and redo operations''queue commands for batch execution''log command execution'

NOT This Pattern When

Not the Strategy Pattern - which changes algorithm behavior, not command encapsulation

Similar Problems

Memento Pattern - for state snapshot and restorationObserver Pattern - for event notification on command execution

Practice

(1/5)
1. In which scenario is encapsulation most beneficial when designing a class?
easy
A. When you want to expose all internal data directly for easy access
B. When you want to bundle data and methods while restricting direct access to internal state
C. When you want to avoid using any access modifiers and rely on global variables
D. When you want to implement multiple inheritance to reuse code

Solution

  1. Step 1: Understand encapsulation purpose

    Encapsulation bundles data and methods and restricts direct access to internal state to protect object integrity.
  2. Step 2: Analyze options

    When you want to expose all internal data directly for easy access exposes internal data directly, violating encapsulation. When you want to avoid using any access modifiers and rely on global variables ignores access control, risking data corruption. When you want to implement multiple inheritance to reuse code relates to inheritance, not encapsulation.
  3. Final Answer:

    Option B -> Option B
  4. Quick Check:

    Encapsulation is about bundling and controlled access, not exposing all data or inheritance.
Hint: Encapsulation bundles and hides, not exposes [OK]
Common Mistakes:
  • Confusing encapsulation with inheritance
  • Thinking encapsulation means no access at all
  • Believing global variables are encapsulated
2. When a class inherits from multiple classes that have a method with the same name, describe the step-by-step process the Method Resolution Order (MRO) uses to determine which method is called.
easy
A. MRO uses a linearization algorithm that merges the order of parents and their ancestors to find the method.
B. MRO searches the first parent class fully before moving to the next parent class.
C. MRO always calls the method from the last parent class listed in the inheritance.
D. MRO randomly picks the method from any parent class that defines it.

Solution

  1. Step 1: Understand naive search

    MRO does not simply search the first parent class fully before moving to the next; it uses a more sophisticated approach.
  2. Step 2: Recognize MRO linearization

    MRO uses a specific linearization (like C3 linearization) that merges parent classes and their ancestors in a consistent order.
  3. Step 3: Eliminate incorrect options

    MRO always calls the method from the last parent class listed in the inheritance is incorrect because the last parent is not always chosen; order and ancestors matter. MRO randomly picks the method from any parent class that defines it is incorrect because MRO is deterministic, not random.
  4. Final Answer:

    Option A -> Option A
  5. Quick Check:

    MRO merges inheritance hierarchies to find the correct method in a predictable order.
Hint: MRO = deterministic linearization of inheritance graph
Common Mistakes:
  • Assuming simple left-to-right search suffices
  • Believing last parent always overrides
  • Thinking method choice is random
3. Identify the bug in the following Python code implementing deep copy for the Profile class:
import copy

class Profile:
    def __init__(self, name, scores):
        self.name = name
        self.scores = scores

    def __deepcopy__(self, memo):
        new_name = self.name  # Bug here
        new_scores = copy.deepcopy(self.scores, memo)
        return Profile(new_name, new_scores)

original = Profile('Alice', [10, 20])
copy_obj = copy.deepcopy(original)
copy_obj.scores.append(30)
print(original.scores)
medium
A. Line assigning new_name = self.name does not deepcopy the name string
B. Line assigning new_scores = copy.deepcopy(self.scores, memo) incorrectly copies scores
C. The __init__ method does not initialize scores properly
D. The return statement returns a new Profile instead of modifying self

Solution

  1. Step 1: Examine __deepcopy__ method

    The line new_name = self.name copies the reference to the name string instead of deep copying it.
  2. Step 2: Understand impact

    Strings are immutable in Python, so shallow copy is usually safe, but if name were a mutable object, this would cause shared references and bugs. Proper deep copy should be used for consistency.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Only new_name assignment lacks deepcopy -> subtle bug [OK]
Hint: All nested fields must be deep copied consistently [OK]
Common Mistakes:
  • Forgetting to deepcopy all nested fields, assuming immutables are safe
4. What is the time complexity of calling the prepare_recipe method in the Template Method Pattern implementation for a beverage, assuming each step runs in constant time?
medium
A. O(1), since the number of steps is fixed and each step runs in constant time
B. O(log n), due to the hook method optimizing optional steps
C. O(n^2), because each step may call other steps recursively
D. O(n), where n is the number of steps in the recipe

Solution

  1. Step 1: Identify number of steps

    The template method defines a fixed sequence of steps (boil_water, brew, pour_in_cup, add_condiments).
  2. Step 2: Analyze step execution time

    Each step runs in constant time; the hook method only conditionally calls add_condiments but does not affect asymptotic complexity.
  3. Final Answer:

    Option D -> Option D
  4. Quick Check:

    Fixed steps with constant time each -> O(1) total [OK]
Hint: Fixed step count -> constant time complexity [OK]
Common Mistakes:
  • Confusing n as input size
  • Assuming recursion adds complexity
5. Examine the following buggy code implementing the Template Method Pattern. Which line contains the subtle bug that breaks the pattern's intended behavior?
medium
A. Line overriding prepare_recipe in Tea subclass
B. Line defining abstract method brew in base class
C. Line calling add_condiments inside prepare_recipe base method
D. Line overriding customer_wants_condiments in Tea subclass

Solution

  1. Step 1: Identify overridden methods

    Tea overrides prepare_recipe, which breaks the template method pattern by duplicating and changing the algorithm flow.
  2. Step 2: Understand impact

    Overriding the template method in subclass bypasses the base class skeleton, causing inconsistent behavior and code duplication.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Template method must not be overridden by subclasses [OK]
Hint: Overriding template method breaks algorithm skeleton [OK]
Common Mistakes:
  • Thinking overriding abstract methods is bug
  • Ignoring hook method usage