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
- Strategy Pattern β Commands look like strategies but carry state
- SOLID Principles β Commands support SRP (each command = one operation)
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
-
β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.
-
β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.
-
βHow do you handle macro commands (multiple steps)?β β Create a
MacroCommandthat holds a list of sub-commands. Execute runs all in order; undo reverses in opposite order. Transfer is already an example of this. -
β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.