Bird
Raised Fist0
Interview Prepoop-design-patternshardAmazonGoogleFlipkartCREDRazorpay

Command Pattern - Undo/Redo, Request Queuing & Logging

Choose your preparation mode3 modes available
🎯
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. You are designing a system for different types of vehicles where some can fly and some can float on water. Which design approach best supports adding new capabilities like flying or floating without modifying existing vehicle classes?
easy
A. Use inheritance to create subclasses like FlyingCar and FloatingCar from Vehicle
B. Use a single Vehicle class with flags indicating if it can fly or float
C. Use composition by creating separate capability classes like FlyBehavior and FloatBehavior and compose them with Vehicle
D. Use inheritance and override methods in each subclass to add flying or floating behavior

Solution

  1. Step 1: Identify the problem with inheritance here

    Inheritance leads to a combinatorial explosion of subclasses (FlyingCar, FloatingCar, FlyingFloatingCar), making the design rigid and hard to maintain.
  2. Step 2: Understand composition benefits

    Composition allows attaching behaviors dynamically via separate classes (FlyBehavior, FloatBehavior), promoting flexibility and easier extension.
  3. Step 3: Analyze other options

    Options A and C rely on inheritance, causing tight coupling and poor scalability. Use a single Vehicle class with flags indicating if it can fly or float uses flags, which leads to complex conditional logic and violates single responsibility.
  4. Final Answer:

    Option C -> Option C
  5. Quick Check:

    Composition supports adding new capabilities without modifying existing classes, enhancing maintainability and flexibility.
Hint: Composition lets you add capabilities by combining behaviors, inheritance forces rigid hierarchies.
Common Mistakes:
  • Assuming inheritance is always better for code reuse
  • Using flags instead of behaviors leads to messy conditionals
2. Given the following Python code using the Template Method Pattern, what is the exact output when calling Tea().prepare_recipe()?
easy
A. Boil water Steep tea bag Pour into cup
B. Boil water Pour into cup Steep tea bag
C. Steep tea bag Boil water Pour into cup
D. Boil water Steep tea bag Pour into cup Add lemon

Solution

  1. Step 1: Trace prepare_recipe steps

    Calls boil_water (prints 'Boil water'), then brew (prints 'Steep tea bag'), then pour_in_cup (prints 'Pour into cup').
  2. Step 2: Check hook method customer_wants_condiments

    Tea overrides it to return False, so add_condiments is skipped.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Condiments skipped due to hook returning False [OK]
Hint: Hook method controls optional step execution [OK]
Common Mistakes:
  • Assuming condiments always added
  • Mixing order of steps
3. Identify the bug in the following code snippet implementing the strategy pattern for payment processing:
class PaymentProcessor:
    def __init__(self, strategy: PaymentStrategy):
        self.strategy = strategy

    def pay(self, amount):
        if isinstance(self.strategy, CreditCardStrategy):
            print(f"Processing credit card payment of ${amount}")
        elif isinstance(self.strategy, UPIStrategy):
            print(f"Processing UPI payment of ${amount}")
        else:
            print("Invalid payment method")
medium
A. The bug is that the pay method uses conditional checks instead of polymorphism.
B. The bug is missing a call to the strategy's pay method inside pay().
C. The bug is in the constructor where the strategy is not assigned properly.
D. The bug is that the else clause should raise an exception instead of printing.

Solution

  1. Step 1: Analyze the pay method implementation

    The pay method uses isinstance checks and conditionals to decide behavior, which defeats the purpose of polymorphism.
  2. Step 2: Identify correct polymorphic usage

    The correct approach is to call self.strategy.pay(amount) directly, letting each strategy handle its own payment logic.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Using conditionals in pay() breaks the strategy pattern [OK]
Hint: Strategy pattern requires polymorphic calls, not conditionals [OK]
Common Mistakes:
  • Using type checks instead of polymorphism
4. If you want to allow decorators to be added and removed dynamically at runtime (e.g., undo last added decorator), which modification to the Decorator Pattern implementation is most appropriate?
hard
A. Modify the base class to include flags for each decorator feature
B. Use inheritance to create new subclasses for every add/remove combination
C. Store decorators in a stack data structure and update the wrapped object reference on add/remove
D. Cache all possible decorator combinations in a lookup table

Solution

  1. Step 1: Understand dynamic add/remove requirement

    Need to add or remove decorators at runtime without subclass explosion or code changes.
  2. Step 2: Identify suitable data structure and pattern modification

    Using a stack to track decorators allows pushing and popping decorators, updating the wrapped object reference accordingly.
  3. Step 3: Evaluate other options

    Inheritance and flags lead to rigidity; caching all combinations is infeasible for many decorators.
  4. Final Answer:

    Option C -> Option C
  5. Quick Check:

    Stack structure supports dynamic decorator management efficiently [OK]
Hint: Stack supports dynamic add/remove of decorators [OK]
Common Mistakes:
  • Trying to use inheritance for dynamic behavior
  • Modifying base class breaks open-closed principle
5. Consider a subclass that overrides a method with a covariant return type but also changes the method's side effects in a way that violates the superclass's behavioral contract. How should this be addressed to maintain LSP compliance?
hard
A. Allow the side effect changes since the return type is covariant and thus safe.
B. Ignore side effects as they are not part of the method signature and thus irrelevant to LSP.
C. Refactor the subclass to preserve the original side effects or weaken them, ensuring behavioral compatibility.
D. Change the superclass method to accommodate the subclass's side effects.

Solution

  1. Step 1: Understand LSP behavioral contract

    LSP requires that subclasses preserve the observable behavior of the superclass, including side effects.
  2. Step 2: Analyze covariant return type

    Covariant return types are allowed and safe, but side effects must still comply with the superclass contract.
  3. Step 3: Evaluate options

    Refactor the subclass to preserve the original side effects or weaken them, ensuring behavioral compatibility. correctly suggests refactoring to preserve or weaken side effects. Allow the side effect changes since the return type is covariant and thus safe. is incorrect; side effect changes can break clients. Ignore side effects as they are not part of the method signature and thus irrelevant to LSP. is false; side effects are part of behavior and relevant. Change the superclass method to accommodate the subclass's side effects. is risky and breaks superclass abstraction.
  4. Final Answer:

    Option C -> Option C
  5. Quick Check:

    Behavioral compatibility includes side effects, not just signatures.
Hint: Covariant returns are safe, but side effects must not violate superclass behavior.
Common Mistakes:
  • Ignoring side effects in LSP
  • Assuming covariant return types cover all behavioral changes
  • Modifying superclass to fit subclass
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard