Definition
- Command pattern – encapsulates a request as an object, bundling the action, its parameters, and its receiver into a storable, passable unit.
- Command interface – declares
execute()and usuallyundo(). - Concrete command – holds a reference to the receiver and parameters; its
execute()calls the receiver's real method. - Receiver – the object that does the actual work, such as
AccountServicewithdebit()andcredit()methods. - Invoker – holds and triggers commands; keeps a history stack for undo and a redo stack for redo.
- Decoupling – the invoker only knows the
Commandinterface, never the receiver.
At National Bank of India, instead of a teller directly moving money, each instruction is written on a slip: debit this account by this amount. The slip is a command. A clerk (invoker) processes slips from a tray without knowing the details. The vault (receiver) actually performs the work. A copy of every slip goes into a log. If a batch must be reversed, each slip has a matching reversal, so the bank can undo the whole batch slip by slip.
Understanding the Concepts
What is the Command pattern?
The Command pattern encapsulates a request as an object, so an action, its parameters, and its receiver are bundled together. This lets you store, pass, queue, log, and undo operations.
package com.bank.command;
public interface Command {
void execute();
void undo();
}
How do you implement concrete commands?
A concrete command holds a reference to the receiver and the parameters. Its execute calls the receiver's real method, and its undo performs the reverse.
package com.bank.command;
import java.math.BigDecimal;
public class DebitCommand implements Command {
private final Account account;
private final BigDecimal amount;
public DebitCommand(Account account, BigDecimal amount) {
this.account = account;
this.amount = amount;
}
@Override
public void execute() {
account.debit(amount);
System.out.println("Debited INR " + amount + " from " + account.getId());
}
@Override
public void undo() {
account.credit(amount);
System.out.println("Reversed debit of INR " + amount + " to " + account.getId());
}
}
public class CreditCommand implements Command {
private final Account account;
private final BigDecimal amount;
public CreditCommand(Account account, BigDecimal amount) {
this.account = account;
this.amount = amount;
}
@Override
public void execute() {
account.credit(amount);
System.out.println("Credited INR " + amount + " to " + account.getId());
}
@Override
public void undo() {
account.debit(amount);
System.out.println("Reversed credit of INR " + amount + " from " + account.getId());
}
}
How does the invoker enable undo?
The invoker keeps a history stack. Each time a command executes, it is pushed onto the stack. Undo pops the last command and calls its undo(). A redo stack enables redo.
package com.bank.command;
import java.util.ArrayDeque;
import java.util.Deque;
public class TransactionInvoker {
private final Deque<Command> history = new ArrayDeque<>();
private final Deque<Command> redoStack = new ArrayDeque<>();
public void execute(Command cmd) {
cmd.execute();
history.push(cmd);
redoStack.clear(); // new action invalidates redo
}
public void undo() {
if (history.isEmpty()) throw new IllegalStateException("Nothing to undo");
Command cmd = history.pop();
cmd.undo();
redoStack.push(cmd);
}
public void redo() {
if (redoStack.isEmpty()) throw new IllegalStateException("Nothing to redo");
Command cmd = redoStack.pop();
cmd.execute();
history.push(cmd);
}
}
TransactionInvoker invoker = new TransactionInvoker();
Account rajesh = new Account("SAV2001", new BigDecimal("100000"));
invoker.execute(new DebitCommand(rajesh, new BigDecimal("25000")));
invoker.execute(new CreditCommand(rajesh, new BigDecimal("5000")));
invoker.undo(); // reverses the credit
invoker.undo(); // reverses the debit
Debited INR 25000 from SAV2001
Credited INR 5000 to SAV2001
Reversed credit of INR 5000 from SAV2001
Reversed debit of INR 25000 to SAV2001
How does queueing work?
Because commands are objects, they can be put on a queue and executed later by a worker. This enables batch processing, scheduling, and retry.
package com.bank.command;
import java.util.LinkedList;
import java.util.Queue;
public class BatchProcessor {
private final Queue<Command> queue = new LinkedList<>();
public void enqueue(Command cmd) { queue.add(cmd); }
public void processAll() {
while (!queue.isEmpty()) {
queue.poll().execute();
}
}
}
BatchProcessor batch = new BatchProcessor();
batch.enqueue(new DebitCommand(rajesh, new BigDecimal("10000")));
batch.enqueue(new CreditCommand(priya, new BigDecimal("10000")));
batch.processAll(); // processes transfer as a batch
How does logging work?
Each command can describe or serialise itself, so you write it to an audit log before executing. This gives a replayable trail.
public void execute(Command cmd) {
auditLog.write(cmd.toString()); // log before execution
cmd.execute();
history.push(cmd);
}
// The audit log can be replayed to reconstruct state
How do macro commands work?
A macro command composes several commands into one. Its execute runs them in order, and its undo runs their undos in reverse order.
package com.bank.command;
import java.util.List;
public class TransferMacro implements Command {
private final List<Command> steps;
public TransferMacro(Account from, Account to,
java.math.BigDecimal amount) {
this.steps = List.of(
new DebitCommand(from, amount),
new CreditCommand(to, amount)
);
}
@Override
public void execute() {
steps.forEach(Command::execute);
}
@Override
public void undo() {
// Undo in reverse order
for (int i = steps.size() - 1; i >= 0; i--) {
steps.get(i).undo();
}
}
}
Command transfer = new TransferMacro(rajesh, priya, new BigDecimal("50000"));
transfer.execute(); // debits rajesh, credits priya
transfer.undo(); // credits rajesh, debits priya
What are common mistakes?
The first mistake is not capturing enough state for undo. For a transfer, the command must store both accounts and the amount, not just the action.
The second mistake is not guarding against double-undo, which would reverse an already-reversed operation.
// Wrong: undo can be called multiple times
cmd.undo(); // first undo: correct
cmd.undo(); // second undo: double-reversal, wrong balance
// Correct: track execution state
private boolean executed = false;
@Override
public void execute() {
if (executed) throw new IllegalStateException("Already executed");
account.debit(amount);
executed = true;
}
What should a senior engineer know?
In modern Java, a simple Command is often just a Runnable, which is why task queues and executors are Command-style. The pattern earns its full weight when you need undo, auditability, or composition.
For money, undo correctness is critical. Pankaj insists on exact inverse operations, guards against double-undo, and keeps commands immutable and self-contained so they are safe to queue and replay across threads.
// Modern Java: Command as Runnable
Runnable cmd = () -> account.debit(new BigDecimal("10000"));
executorService.submit(cmd); // queued for later execution
// Full Command pattern: when you need undo and audit
Command cmd = new DebitCommand(account, new BigDecimal("10000"));
invoker.execute(cmd); // logged, undoable, composable
Visual Overview
Command Pattern Roles
─────────────────────
Invoker ──execute()──→ Command ──→ Receiver
│ │
│ history stack │ DebitCommand
│ [cmd3, cmd2, cmd1] │ holds: account + amount
│ │ execute: account.debit()
│ undo() → pop cmd3 │ undo: account.credit()
│ cmd3.undo() │
Transfer as Macro Command
─────────────────────────
TransferMacro
│
├── DebitCommand(rajesh, 50000) ←── execute order: 1
└── CreditCommand(priya, 50000) ←── execute order: 2
Undo order: 2, 1 (reverse)
Key Takeaway
The Command pattern encapsulates a request as an object, so an action, its parameters, and its receiver are bundled together into something that can be stored, queued, logged, and undone. It has four roles: a command interface with execute and undo, concrete commands that call operations on a receiver, the receiver that does the real work, and an invoker that triggers commands without knowing the receiver.
Undo works because each command knows how to reverse itself. The invoker keeps a history stack, and undo pops the last command and calls its undo. Queueing works because commands are objects, so they can be put on a queue and run later. Logging works because each command can describe itself, giving a replayable audit trail. And a macro command composes several commands into one: execute runs them in order, undo runs their undos in reverse.
In the end-of-day batch at National Bank of India, thousands of debits and credits are represented as command objects and enqueued. The invoker logs each one before executing. If a batch fails partway, Rajesh undoes the executed commands in reverse order to roll back cleanly. A transfer is a macro of a debit and a credit, and undoing the whole batch returns every account to its starting balance.
The senior point is that in modern Java a simple command is often just a Runnable, so executors and task queues are essentially Command-style. The pattern earns its full weight when you need undo, auditability, or composition. For money, undo correctness is critical: exact inverse operations, double-undo guards, and immutable, self-contained commands for thread safety.
Remember one sentence: Command turns an action into an object with execute and undo, enabling queuing, logging, replay, and reversal while fully decoupling the invoker from the receiver.
---