Back to Behavioral: Strategy, Observer, Command, State, Template
Q9Question 9 of 12

Design using the Command pattern

Key Concepts
  • The Command pattern turns an action into an object that can be executed, queued, logged, and undone.
  • A command interface declares `execute()` and `undo()`; each concrete command captures its receiver and parameters.
  • An invoker holds a history stack for undo and a redo stack for redo, operating on command objects.
  • Because commands are objects, they can be queued for batch processing, serialised for audit, or retried on failure.
  • Each command must capture enough state to reverse itself exactly; for money operations, exact inverse is critical.
  • Macro commands compose several commands into one, running execute in order and undo in reverse.

Definition

The Command pattern turns an action into an object that can be executed, queued, logged, and undone.
  • 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 Account with debit() and credit() 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.

JAVA
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.

JAVA
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.

JAVA
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);
    }
}
JAVA
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());
    }
}
TEXT
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.

JAVA
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();
        }
    }
}
JAVA
BatchQueue batch = new BatchQueue();
batch.enqueue(new TransferCommand(rajesh, priya, new BigDecimal("10000")));
batch.enqueue(new TransferCommand(priya, rajesh, new BigDecimal("5000")));
batch.processAll();
TEXT
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.

JAVA
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.

JAVA
// 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.

JAVA
// 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.

JAVA
// 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

DIAGRAM
  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]
DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate