Definition
- Command – encapsulates a request as an object, carrying everything needed to perform and reverse the action.
- execute() – performs the action by calling the receiver's method.
- undo() – reverses the action, either by performing the exact inverse or restoring a snapshot.
- Invoker – holds commands, runs them, and maintains history and redo stacks.
- Receiver – the object that does the real work, such as an
Accountwithdebit()andcredit()methods.
Think of the Command as a signed instruction slip at National Bank of India. When Rajesh wants to transfer money, he fills a slip describing the action. The bank can execute it now, queue it for later, keep it in a log, and if something goes wrong, perform the exact reverse. Each slip knows both how to do its action and how to undo it.
Understanding the Concepts
What is the Command pattern for undo/redo?
A command interface declares execute() and undo(). Each concrete command captures its receiver and parameters, making it a self-contained unit of work. An invoker runs commands and keeps a history stack for undo and a redo stack for redo.
package com.bank.command;
public interface BankCommand {
void execute();
void undo();
String describe();
}
How do you implement transfer commands?
Each command holds the account and amount. Execute performs the action, undo performs the reverse.
package com.bank.command;
import java.math.BigDecimal;
public class TransferCommand implements BankCommand {
private final Account from;
private final Account to;
private final BigDecimal amount;
public TransferCommand(Account from, Account to, BigDecimal amount) {
this.from = from;
this.to = to;
this.amount = amount;
}
@Override
public void execute() {
from.debit(amount);
to.credit(amount);
System.out.println("Transferred INR " + amount
+ " from " + from.getId() + " to " + to.getId());
}
@Override
public void undo() {
to.debit(amount);
from.credit(amount);
System.out.println("Reversed transfer of INR " + amount);
}
@Override
public String describe() {
return "Transfer INR " + amount + " from "
+ from.getId() + " to " + to.getId();
}
}
How does the invoker manage undo and redo?
The invoker keeps a history stack and a redo stack. Execute pushes to history and clears redo. Undo pops from history, calls undo, and pushes to redo. Redo pops from redo, calls execute, and pushes to history.
package com.bank.command;
import java.util.ArrayDeque;
import java.util.Deque;
public class BankInvoker {
private final Deque<BankCommand> history = new ArrayDeque<>();
private final Deque<BankCommand> redoStack = new ArrayDeque<>();
public void execute(BankCommand cmd) {
cmd.execute();
history.push(cmd);
redoStack.clear();
}
public void undo() {
if (history.isEmpty()) {
System.out.println("Nothing to undo");
return;
}
BankCommand cmd = history.pop();
cmd.undo();
redoStack.push(cmd);
}
public void redo() {
if (redoStack.isEmpty()) {
System.out.println("Nothing to redo");
return;
}
BankCommand cmd = redoStack.pop();
cmd.execute();
history.push(cmd);
}
}
package com.bank.demo;
import com.bank.command.*;
import java.math.BigDecimal;
public class CommandDemo {
public static void main(String[] args) {
Account rajesh = new Account("SAV2001", new BigDecimal("100000"));
Account priya = new Account("SAV3002", new BigDecimal("50000"));
BankInvoker invoker = new BankInvoker();
invoker.execute(new TransferCommand(
rajesh, priya, new BigDecimal("25000")));
System.out.println("Rajesh: " + rajesh.getBalance());
System.out.println("Priya: " + priya.getBalance());
invoker.undo();
System.out.println("After undo - Rajesh: " + rajesh.getBalance());
System.out.println("After undo - Priya: " + priya.getBalance());
invoker.redo();
System.out.println("After redo - Rajesh: " + rajesh.getBalance());
}
}
Transferred INR 25000 from SAV2001 to SAV3002
Rajesh: 75000
Priya: 75000
Reversed transfer of INR 25000
After undo - Rajesh: 100000
After undo - Priya: 50000
Transferred INR 25000 from SAV2001 to SAV3002
After redo - Rajesh: 75000
How do you queue commands for batch execution?
Because commands are objects, they can be enqueued and processed later.
package com.bank.command;
import java.util.LinkedList;
import java.util.Queue;
public class BatchQueue {
private final Queue<BankCommand> queue = new LinkedList<>();
public void enqueue(BankCommand cmd) {
System.out.println("Queued: " + cmd.describe());
queue.add(cmd);
}
public void processAll() {
System.out.println("Processing batch of " + queue.size() + " commands");
while (!queue.isEmpty()) {
queue.poll().execute();
}
}
}
BatchQueue batch = new BatchQueue();
batch.enqueue(new TransferCommand(rajesh, priya, new BigDecimal("10000")));
batch.enqueue(new TransferCommand(priya, rajesh, new BigDecimal("5000")));
batch.processAll();
Queued: Transfer INR 10000 from SAV2001 to SAV3002
Queued: Transfer INR 5000 from SAV3002 to SAV2001
Processing batch of 2 commands
Transferred INR 10000 from SAV2001 to SAV3002
Transferred INR 5000 from SAV3002 to SAV2001
How do you log commands for audit?
Each command's describe() method provides a human-readable representation for the audit log.
public void execute(BankCommand cmd) {
auditLog.info("EXECUTING: " + cmd.describe());
cmd.execute();
history.push(cmd);
}
What are common mistakes?
The first mistake is not capturing enough state for undo. A transfer must store both accounts and the amount.
The second mistake is allowing double-execution or double-undo, which corrupts balances.
// Wrong: no guard against double execution
cmd.execute(); // first time: correct
cmd.execute(); // second time: double debit
// Correct: track execution state
private boolean executed = false;
public void execute() {
if (executed) throw new IllegalStateException("Already executed");
from.debit(amount);
to.credit(amount);
executed = true;
}
When should you use the Command pattern?
Use it when you need undo/redo, batch processing, audit logging, or operation composition. Do not over-engineer simple operations that do not need reversal.
// Good use case: bank transfer that needs undo and audit
BankCommand cmd = new TransferCommand(rajesh, priya, amount);
invoker.execute(cmd);
// Over-engineering: simple read-only query
BankCommand cmd = new GetBalanceCommand(rajesh); // no undo needed
What should a senior engineer know?
The crucial detail for undo is that each command must capture enough state to reverse itself exactly. For a transfer, that means the amount and both accounts. Pankaj keeps commands immutable and self-contained so they are safe to queue and replay across threads. In modern Java, a simple fire-and-forget action is just a Runnable; the Command pattern earns its weight when undo, audit, or composition is needed.
// Simple case: Runnable is enough
executor.submit(() -> account.debit(amount));
// Complex case: full Command for undo + audit
invoker.execute(new TransferCommand(from, to, amount));
Visual Overview
Undo/Redo with Command Pattern
──────────────────────────────
execute(cmd1) → history: [cmd1]
execute(cmd2) → history: [cmd2, cmd1]
execute(cmd3) → history: [cmd3, cmd2, cmd1]
undo() → cmd3.undo(), redo: [cmd3]
history: [cmd2, cmd1]
undo() → cmd2.undo(), redo: [cmd2, cmd3]
history: [cmd1]
redo() → cmd2.execute(), history: [cmd2, cmd1]
redo: [cmd3]
Batch Queue
───────────
Queue: [TransferCmd1, TransferCmd2, TransferCmd3]
│
▼
processAll() → execute each in order
Key Takeaway
The Command pattern encapsulates a request as an object that carries everything needed both to perform an action and to reverse it. By turning an operation into a first-class object, you gain the ability to queue, log, and undo operations, and you decouple the invoker from the receiver.
The structure is a command interface with execute and undo, concrete commands that capture their receiver and parameters, and an invoker that runs commands and keeps a history stack. Undo pops the last command and calls its undo, and a redo stack enables redo. Because commands are objects, they can be queued for batch processing, serialised into an audit trail, or retried on failure.
The crucial detail for undo is that each command must capture enough state to reverse itself exactly. For a transfer, that means the amount and both accounts. For operations that are not naturally reversible, store a compensating action rather than pretending undo is trivial. Keep commands idempotent where retries are possible.
At National Bank of India, modelling a transfer as a command is what lets a support agent cleanly reverse an accidental transfer with a single undo that restores both balances precisely, and it is what lets Rajesh queue operations for batch execution and keep a complete, replayable audit log.
Remember one sentence: the Command pattern turns an action into an object with execute and undo, enabling queuing, logging, and reversal while decoupling the invoker from the receiver.
---