Back to Behavioral: Strategy, Observer, Command, State, Template
Q6Question 6 of 12

What is the Command pattern and how does it enable undo, queueing, logging, and macros

Key Concepts
  • Command turns an action into an object with `execute()` and `undo()`, decoupling the invoker from the receiver.
  • The four roles are: command interface, concrete commands, the receiver that does the real work, and the invoker that triggers commands.
  • Undo works because each command stores the state or inverse operation needed to reverse itself; the invoker keeps a history stack.
  • Queueing works because commands are objects that can be stored, scheduled, and executed later by a worker.
  • Logging works because each command can be serialised and written to an audit trail for replay.
  • Macro commands compose several commands into one, running them in order and undoing them in reverse.

Definition

Command turns an action into an object with execute and undo, so the invoker can queue, log, replay, undo, and combine actions without knowing the receiver.
  • 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 usually undo().
  • 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 AccountService with debit() and credit() methods.
  • Invoker – holds and triggers commands; keeps a history stack for undo and a redo stack for redo.
  • Decoupling – the invoker only knows the Command interface, 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.

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

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

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

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

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

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

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

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

DIAGRAM
  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()         │
DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate