The shopping cart of an online store needs three things that direct method calls cannot give it: an "Undo" button after a customer accidentally deletes an item, a "Redo" button when they change their mind, and a log of every action for reconciliation. On top of that, post-order jobs (sending email, syncing accounting, calling the carrier) should be queued and retried on failure. Both needs have the same answer: turn each request into an object. That is the Command pattern. This article builds the Command pattern from scratch in Java 21 (the code only needs Java 17 or later) with two undo/redo stacks, adds composite commands (macros), builds a command queue with retries and a dead-letter list, and then compares the result with the JDK's UndoManager. Every snippet was run on JDK 21, and the output is pasted verbatim.
The two previous articles in the series are Template Method: algorithm skeletons and extension points and Observer pattern: from listeners to event buses and the Flow API. The Command pattern often goes hand in hand with those two: Observer announces that "something happened", and Command is the packaged "thing to do".
What problem the Command pattern solves
According to refactoring.guru, the Command pattern turns a request into a stand-alone object that contains all the information needed to perform it. That lets you pass requests as arguments, delay or queue them, and support undo. The Gang of Four describe the same idea: separate the object that invokes an operation from the object that knows how to perform it. There are four main roles:
| Role | Responsibility | In the shopping cart example |
|---|---|---|
| Command | Interface with execute() (and undo() if undo is supported) | Command |
| Concrete command | Holds the receiver and parameters, and forwards the call to the receiver | AddItem, RemoveAll |
| Receiver | Contains the real business logic | Cart |
| Invoker | Triggers commands and keeps the history, without knowing what a command does | History |
The client creates the command, wires it to the receiver, and hands it to the invoker. The benefits of the Command pattern are Single Responsibility (the invoker holds no business logic) and Open/Closed (adding a new command does not change the invoker). The cost is an extra layer of indirection, which is sometimes not worth it in a small application.
Implementing the Command pattern for a shopping cart
In this Command pattern example, the receiver is a cart that only knows how to add and remove quantities. The Command interface has execute, undo, and describe (for display in a menu or a log):
static class Cart {
private final Map<String, Integer> items = new TreeMap<>();
void add(String sku, int qty) { items.merge(sku, qty, Integer::sum); }
void remove(String sku, int qty) {
int left = items.getOrDefault(sku, 0) - qty;
if (left <= 0) items.remove(sku); else items.put(sku, left);
}
int qty(String sku) { return items.getOrDefault(sku, 0); }
}
interface Command {
void execute();
void undo();
String describe();
}
record AddItem(Cart cart, String sku, int qty) implements Command {
public void execute() { cart.add(sku, qty); }
public void undo() { cart.remove(sku, qty); }
public String describe() { return "add " + qty + " x " + sku; }
}AddItem has an obvious inverse: if you add n, undo removes n. Not every command in the Command pattern is that lucky. "Remove all of an item" cannot work out by itself how much to restore, so the command has to remember the state before it runs:
static final class RemoveAll implements Command {
private final Cart cart; private final String sku; private int removed;
RemoveAll(Cart cart, String sku) { this.cart = cart; this.sku = sku; }
public void execute() { removed = cart.qty(sku); cart.remove(sku, removed); }
public void undo() { if (removed > 0) cart.add(sku, removed); }
public String describe() { return "remove all " + sku; }
}For undo in the Command pattern, refactoring.guru names exactly two approaches: store a copy of the state (memory-hungry, and hard when the state is private; usually combined with Memento) or perform the inverse operation. RemoveAll is a hybrid: it stores only what is strictly needed, a single integer.
Undo/redo history for the Command pattern with two stacks
The History invoker keeps two stacks. When a new command runs, it executes, is pushed onto the undo stack, and the redo stack is cleared. On undo, it takes the top command, calls undo(), and moves it to the redo stack. Redo does the opposite:
static class History {
private final Deque<Command> undo = new ArrayDeque<>();
private final Deque<Command> redo = new ArrayDeque<>();
void run(Command c) {
c.execute();
undo.push(c);
redo.clear(); // a new command discards the redo branch
}
boolean undo() {
if (undo.isEmpty()) return false;
Command c = undo.pop(); c.undo(); redo.push(c); return true;
}
boolean redo() {
if (redo.isEmpty()) return false;
Command c = redo.pop(); c.execute(); undo.push(c); return true;
}
}The scenario in CartDemo.java walks through every branch, including the composite command explained in the next section:
add 2 t-shirt cart={t-shirt=2} undo=1 redo=0
add 1 shoes cart={shoes=1, t-shirt=2} undo=2 redo=0
remove all t-shirt cart={shoes=1} undo=3 redo=0
undo cart={shoes=1, t-shirt=2} undo=2 redo=1
undo cart={t-shirt=2} undo=1 redo=2
redo cart={shoes=1, t-shirt=2} undo=2 redo=1
new command (redo branch lost) cart={cap=3, shoes=1, t-shirt=2} undo=3 redo=0
can redo? false
macro: macro 'combo-hanoi' (2 steps) cart={cap=4, shoes=1, t-shirt=3} undo=4 redo=0
undo whole macro cart={cap=3, shoes=1, t-shirt=2} undo=3 redo=1The first undo shows that undo restores exactly the 2 t-shirts that were removed, because RemoveAll remembered the quantity. The redo line is the one people often misread: after two undos and one redo, one command is still in the redo stack (redo=1). Adding the "new command (redo branch lost)" clears that stack down to 0, so the "can redo?" line returns false. This is standard behavior in most editors: history is a straight line, not a tree. Keeping the branch would require a more complex history tree, which is rarely worth the effort.
Macros: the Command pattern combined with Composite
Because a command is an object, one command can contain other commands. That is Composite applied to Command, and it turns a single user action ("add combo") into a single undo step:
record Macro(String name, List<Command> steps) implements Command {
public void execute() { steps.forEach(Command::execute); }
public void undo() { for (int i = steps.size() - 1; i >= 0; i--) steps.get(i).undo(); }
public String describe() { return "macro '" + name + "' (" + steps.size() + " steps)"; }
}Two details matter. Undo must run in reverse order (a later step depends on the result of an earlier one), and if a step throws midway, the macro is left half-applied. The demo does not handle that case; a real system has to decide explicitly: roll back the steps that already ran, or wrap the whole macro in a transaction on the receiver. The output above shows a single undo removing both items and returning the cart to {cap=3, shoes=1, t-shirt=2}.
Command queues: delayed execution, retries and dead letters
The Command pattern separates the time of creation from the time of execution, so a command can be put in a queue for a worker to process later. This is the foundation of most background job systems: the JDK's ExecutorService, which accepts a Runnable or Callable, is exactly a Command. The QueueDemo.java example builds a single-threaded worker that takes commands from a BlockingQueue, tries each up to n times, and moves commands that fail for good into a dead-letter list:
interface Job { void run() throws Exception; }
record NamedJob(String name, Job job, int maxAttempts) {}
// worker
NamedJob nj = queue.take();
boolean ok = false;
for (int attempt = 1; attempt <= nj.maxAttempts() && !ok; attempt++) {
try {
nj.job().run();
System.out.println(" [" + nj.name() + "] OK after " + attempt + " attempt(s)");
ok = true;
} catch (Exception e) {
System.out.println(" [" + nj.name() + "] attempt " + attempt + " failed: " + e.getMessage());
}
}
if (!ok) deadLetters.add(nj.name());producer pushes 4 commands onto the queue:
sending email ORD-1
[send-email-ORD1] OK after 1 attempt(s)
[sync-accounting-ORD1] attempt 1 failed: accounting timeout
[sync-accounting-ORD1] attempt 2 failed: accounting timeout
accounting synced for ORD-1
[sync-accounting-ORD1] OK after 3 attempt(s)
[call-carrier-ORD1] attempt 1 failed: 503 from carrier
[call-carrier-ORD1] attempt 2 failed: 503 from carrier
printing invoice ORD-1
[print-invoice-ORD1] OK after 1 attempt(s)
dead-letter: [call-carrier-ORD1]The accounting command failed twice and then succeeded on the third attempt. The carrier command ran out of attempts and went to the dead-letter list, while the invoice command behind it still ran normally: one broken command does not block the queue. There are three things to keep in mind when taking this model to production:
- Idempotency. A command can be run again, so it must be safe to run twice (for example, include the order ID so the receiver can drop duplicates). Sending an email twice annoys the customer, and recording a payment twice loses money.
- In-memory queues are not durable. If the process dies, the commands are gone. To make them durable, store each command as data (command name plus parameters, for example JSON) in a database or message broker, and rebuild the command object when reading it back. A lambda cannot be serialized into data like that, which is why durable job systems usually use explicitly named command classes.
- Backoff. The demo retries immediately; in practice you need to wait for increasing intervals so you do not pile more load onto a failing service.
The Command pattern in the JDK: UndoableEdit and UndoManager
Swing already ships the Command pattern with undo/redo. According to the UndoManager Javadoc, the class manages a list of UndoableEdit objects plus an index pointing at the next edit to undo or redo, is thread-safe, has a default limit of 100 edits (changeable with setLimit), and discards the redo part when a new edit is added. We verify exactly those claims with UndoManagerDemo.java using a limit of 2:
UndoManager default limit is 100, set to 2 here
after 3 edits: 16, Undo add 1
undo: 15, canUndo=true
undo: 10, canUndo=false
the 'add 10' edit was pushed out of history so it cannot be undone, remaining value 10
redo: 15, canRedo=true
new edit: 115, canRedo=false (redo branch discarded)The three edits add 10, 5, and 1, for a total of 16. The limit of 2 pushes the first edit out of history, so two undos only go back to 10, not to 0. This is a deliberate memory trade-off that unlimited history does not have. In your own code, the History above should have the same cap (for example, drop the last element of the ArrayDeque when it exceeds the ceiling), especially if each command holds a large state snapshot.
At the application level, Runnable and Callable are a minimal Command pattern (only execute, no undo), and ExecutorService is an invoker with a queue. In Spring, application events are often used to trigger commands. If commands must survive a restart, the background job system needs to store them as data in a durable store, as described in the queue section.
Command pattern vs Strategy and Memento
| Pattern | What it encapsulates | Key difference |
|---|---|---|
| Command pattern | A specific request with its parameters | Has a lifecycle: create, queue, run, undo, log |
| Strategy pattern | An interchangeable algorithm | Does not hold a specific request; see Strategy pattern with lambdas and functional interfaces |
| Memento pattern | A snapshot of internal state | Only for restoring; the Command pattern can use it for undo |
Structurally, a lambda () -> cart.add("shoes", 1) is both a Strategy and a Command. What distinguishes them is intent: if the object represents a "job to do" that can be stored, delayed, or undone, it is the Command pattern.
Common mistakes when implementing the Command pattern
The Command pattern is simple in structure but has many traps at runtime:
- Forgetting to clear the redo stack on a new command. The result is that redo applies an operation that no longer matches the current state. The
redo.clear()line inHistory.runis not optional. - Undo that is not symmetric with execute. Every command needs a test: run it, undo it, and the state equals the original. A command that depends on prior state must store it, like
RemoveAll. - A stateful command run twice in parallel. A command object with a memo field (like
removed) is not safe to share across threads; create a new command for each use. - History that bloats memory. Set a limit like
UndoManagerdoes and decide explicitly what happens when it is exceeded. - Failure midway through a macro. Decide in advance: stop and undo the steps already done, or skip and continue.
- Commands with side effects that cannot be undone. A sent email cannot be recalled. Mark these commands as not supporting undo, or use a compensating command, such as sending a correction email.
When not to use the Command pattern
If you only need to call a function immediately, with no undo, logging, or queueing, then the Command pattern, meaning a command object, just makes the code longer. For a simple action, passing a plain Runnable or a method reference is enough. The Command pattern also does not replace database transactions: if you need "all or nothing" on durable data, use the database's transaction instead of rolling back by hand with undo(). Finally, when the undo chain has to reflect the actions of many concurrent users (collaborative editing), two local stacks are not enough; that problem needs dedicated techniques such as operational transformation.
Checklist for applying the Command pattern
- In the Command pattern, the command object holds only the receiver, the parameters, and the call; business logic lives in the receiver.
- Every undoable command should be tested with an execute and undo pair.
- A command remembers the state before it runs when its inverse cannot be derived on its own.
- Adding a new command clears the redo stack.
- Limit the size of the history.
- Commands put in a queue must be idempotent and come with a retry policy and a dead-letter list.
- If commands must survive a restart, store them as data, not as lambdas.
Summary
The Command pattern turns requests into objects, which lets us queue, log, retry, and undo them. The core is just an interface with execute and undo, plus an invoker that holds two stacks. The hard part lies in the details: commands that remember prior state, the redo branch being cleared on a new command, the undo order of a macro, and idempotency when a command is run again. The JDK already ships this shape in Runnable, ExecutorService, and UndoManager. The full source is in code/s6-15/; note that run.sh compiles at language level 17 (--source 17) but runs on JDK 21.
Next article
When a request has to pass through several processing steps, and each step decides whether to handle it or pass it on, we use a different pattern: Chain of Responsibility: filter chains in Servlets and Spring Security. To revisit how to separate a skeleton from its varying steps, read Template Method: algorithm skeletons and extension points.



