Limited time: AI code review, hints, mock interviews, whiteboard analysis, and all Pro features are unlocked. Enroll
⏱️ 7 min read

Command Pattern

The Command pattern encapsulates a request as an object, letting you parameterize clients with different requests, queue them, log them, and support undo/redo operations. Each action becomes a self-contained object that knows how to execute itself and (optionally) how to reverse itself.

Why this matters: Whenever a problem mentions β€œundo,” β€œtransaction history,” β€œreplay,” or β€œqueue of operations,” Command is the pattern. Payment wallets, text editors, and task schedulers all benefit from it.


Prerequisites


Class Diagram

classDiagram
    class Command {
        <<interface>>
        +execute()
        +undo()
    }
    class Invoker {
        -history: Stack~Command~
        +executeCommand(Command)
        +undo()
        +redo()
    }
    class CreditCommand {
        -wallet: Wallet
        -amount: Money
        +execute()
        +undo()
    }
    class DebitCommand {
        -wallet: Wallet
        -amount: Money
        +execute()
        +undo()
    }
    class TransferCommand {
        -from: Wallet
        -to: Wallet
        -amount: Money
        +execute()
        +undo()
    }

    Invoker --> Command
    Command <|.. CreditCommand
    Command <|.. DebitCommand
    Command <|.. TransferCommand


Real-World Example: Payment Wallet with Undo

// Command interface
public interface WalletCommand {
    void execute();
    void undo();
    String getDescription();
}

// Concrete command: credit money to wallet
public class CreditCommand implements WalletCommand {
    private final Wallet wallet;
    private final Money amount;
    private final String source;
    private Instant executedAt;

    public CreditCommand(Wallet wallet, Money amount, String source) {
        this.wallet = wallet;
        this.amount = amount;
        this.source = source;
    }

    @Override
    public void execute() {
        wallet.credit(amount);
        this.executedAt = Instant.now();
    }

    @Override
    public void undo() {
        wallet.debit(amount); // Reverse: remove the credited amount
    }

    @Override
    public String getDescription() {
        return "Credit " + amount + " from " + source;
    }
}

// Concrete command: debit money from wallet
public class DebitCommand implements WalletCommand {
    private final Wallet wallet;
    private final Money amount;
    private final String merchant;

    public DebitCommand(Wallet wallet, Money amount, String merchant) {
        this.wallet = wallet;
        this.amount = amount;
        this.merchant = merchant;
    }

    @Override
    public void execute() {
        if (wallet.getBalance().isLessThan(amount)) {
            throw new InsufficientBalanceException(wallet.getBalance(), amount);
        }
        wallet.debit(amount);
    }

    @Override
    public void undo() {
        wallet.credit(amount); // Reverse: restore the debited amount
    }

    @Override
    public String getDescription() {
        return "Debit " + amount + " to " + merchant;
    }
}

// Concrete command: transfer between wallets (composite operation)
public class TransferCommand implements WalletCommand {
    private final Wallet from;
    private final Wallet to;
    private final Money amount;
    private boolean executed = false;

    public TransferCommand(Wallet from, Wallet to, Money amount) {
        this.from = from;
        this.to = to;
        this.amount = amount;
    }

    @Override
    public void execute() {
        if (from.getBalance().isLessThan(amount)) {
            throw new InsufficientBalanceException(from.getBalance(), amount);
        }
        from.debit(amount);
        to.credit(amount);
        executed = true;
    }

    @Override
    public void undo() {
        if (!executed) throw new IllegalStateException("Cannot undo unexecuted command");
        to.debit(amount);
        from.credit(amount);
        executed = false;
    }

    @Override
    public String getDescription() {
        return "Transfer " + amount + " from " + from.getId() + " to " + to.getId();
    }
}

// Invoker: manages execution history, enables undo/redo
public class TransactionManager {
    private final Deque<WalletCommand> history = new ArrayDeque<>();
    private final Deque<WalletCommand> redoStack = new ArrayDeque<>();
    private static final int MAX_HISTORY = 100;

    public void execute(WalletCommand command) {
        command.execute();
        history.push(command);
        redoStack.clear(); // New action invalidates redo history
        if (history.size() > MAX_HISTORY) {
            // Remove oldest to prevent unbounded growth
            ((ArrayDeque<WalletCommand>) history).removeLast();
        }
    }

    public void undo() {
        if (history.isEmpty()) throw new NothingToUndoException();
        WalletCommand command = history.pop();
        command.undo();
        redoStack.push(command);
    }

    public void redo() {
        if (redoStack.isEmpty()) throw new NothingToRedoException();
        WalletCommand command = redoStack.pop();
        command.execute();
        history.push(command);
    }

    public List<String> getTransactionHistory() {
        return history.stream()
            .map(WalletCommand::getDescription)
            .collect(Collectors.toList());
    }
}
from abc import ABC, abstractmethod
from collections import deque
from dataclasses import dataclass
from decimal import Decimal

class WalletCommand(ABC):
    @abstractmethod
    def execute(self): ...
    @abstractmethod
    def undo(self): ...
    @abstractmethod
    def description(self) -> str: ...

class CreditCommand(WalletCommand):
    def __init__(self, wallet: 'Wallet', amount: Decimal, source: str):
        self._wallet = wallet
        self._amount = amount
        self._source = source

    def execute(self):
        self._wallet.credit(self._amount)

    def undo(self):
        self._wallet.debit(self._amount)

    def description(self) -> str:
        return f"Credit {self._amount} from {self._source}"

class DebitCommand(WalletCommand):
    def __init__(self, wallet: 'Wallet', amount: Decimal, merchant: str):
        self._wallet = wallet
        self._amount = amount
        self._merchant = merchant

    def execute(self):
        if self._wallet.balance < self._amount:
            raise InsufficientBalanceError()
        self._wallet.debit(self._amount)

    def undo(self):
        self._wallet.credit(self._amount)

    def description(self) -> str:
        return f"Debit {self._amount} to {self._merchant}"

class TransferCommand(WalletCommand):
    def __init__(self, sender: 'Wallet', receiver: 'Wallet', amount: Decimal):
        self._sender = sender
        self._receiver = receiver
        self._amount = amount

    def execute(self):
        if self._sender.balance < self._amount:
            raise InsufficientBalanceError()
        self._sender.debit(self._amount)
        self._receiver.credit(self._amount)

    def undo(self):
        self._receiver.debit(self._amount)
        self._sender.credit(self._amount)

    def description(self) -> str:
        return f"Transfer {self._amount}"

class TransactionManager:
    def __init__(self, max_history: int = 100):
        self._history: deque[WalletCommand] = deque(maxlen=max_history)
        self._redo_stack: list[WalletCommand] = []

    def execute(self, command: WalletCommand):
        command.execute()
        self._history.append(command)
        self._redo_stack.clear()

    def undo(self):
        if not self._history:
            raise NothingToUndoError()
        command = self._history.pop()
        command.undo()
        self._redo_stack.append(command)

    def redo(self):
        if not self._redo_stack:
            raise NothingToRedoError()
        command = self._redo_stack.pop()
        command.execute()
        self._history.append(command)
#include <stack>
#include <string>
#include <memory>

class WalletCommand {
public:
    virtual ~WalletCommand() = default;
    virtual void execute() = 0;
    virtual void undo() = 0;
    virtual string description() const = 0;
};

class CreditCommand : public WalletCommand {
    Wallet& wallet_;
    double amount_;
    string source_;
public:
    CreditCommand(Wallet& w, double amt, string src)
        : wallet_(w), amount_(amt), source_(std::move(src)) {}

    void execute() override { wallet_.credit(amount_); }
    void undo() override { wallet_.debit(amount_); }
    string description() const override {
        return "Credit " + to_string(amount_) + " from " + source_;
    }
};

class DebitCommand : public WalletCommand {
    Wallet& wallet_;
    double amount_;
    string merchant_;
public:
    DebitCommand(Wallet& w, double amt, string merchant)
        : wallet_(w), amount_(amt), merchant_(std::move(merchant)) {}

    void execute() override {
        if (wallet_.getBalance() < amount_)
            throw InsufficientBalanceException();
        wallet_.debit(amount_);
    }
    void undo() override { wallet_.credit(amount_); }
    string description() const override {
        return "Debit " + to_string(amount_) + " to " + merchant_;
    }
};

class TransactionManager {
    stack<unique_ptr<WalletCommand>> history_;
    stack<unique_ptr<WalletCommand>> redoStack_;
public:
    void execute(unique_ptr<WalletCommand> cmd) {
        cmd->execute();
        history_.push(std::move(cmd));
        while (!redoStack_.empty()) redoStack_.pop();
    }

    void undo() {
        if (history_.empty()) throw NothingToUndoException();
        auto cmd = std::move(history_.top());
        history_.pop();
        cmd->undo();
        redoStack_.push(std::move(cmd));
    }

    void redo() {
        if (redoStack_.empty()) throw NothingToRedoException();
        auto cmd = std::move(redoStack_.top());
        redoStack_.pop();
        cmd->execute();
        history_.push(std::move(cmd));
    }
};

When to Use vs When to Avoid

Use Command When Avoid When
You need undo/redo capability Operations are fire-and-forget
Actions need to be queued, scheduled, or replayed Simple direct method calls suffice
You need transaction logging or audit trails No history requirement exists
Operations need to be composed into macros Each action is trivial and independent
You want to decouple β€œwhen” from β€œwhat” Immediate synchronous execution is fine

Interview Questions

  1. β€œHow do you handle failed undo?” – If undo fails (e.g., insufficient funds to reverse a credit), you need compensation logic or to mark the undo as failed. In practice, idempotent operations help.

  2. β€œCommand vs Strategy – what’s the difference?” – Strategy encapsulates interchangeable algorithms (stateless, swappable). Command encapsulates an action with its parameters (stateful, recordable). Commands know their receiver; strategies don’t.

  3. β€œHow do you handle macro commands (multiple steps)?” – Create a MacroCommand that holds a list of sub-commands. Execute runs all in order; undo reverses in opposite order. Transfer is already an example of this.

  4. β€œWhat about commands that can’t be undone?” – Mark them as non-undoable. The invoker can check before adding to undo history. Example: sending an email can’t truly be undone.


See It in Action

Payment Wallet

Free system design + DSA prep. If it helped you crack an interview, consider supporting.

SensAI SensAI
Beta
Listening...
Tap mic to stop voice mode

Shape what we build next

Every piece of feedback is read by the team and directly influences our roadmap.

What type of feedback?

Install SystemCraft

Add to your home screen for instant access, offline reading, and a distraction-free experience.

Offline reading Faster loads No browser tabs App-like feel

Unlock AI Features

One click to activate - no payment, no credit card. Just sign in and you're in.

AI code review and hints
SensAI chat assistant
AI mock interviews
Whiteboard analysis
100% free during early access