The restaurant management panel in PideYa is a row of buttons: accept order, mark as preparing, notify a delay, cancel. Today each button calls a method directly, the call executes, and it's gone. But the restaurant is asking for things an ephemeral call cannot give: undo the last action (a finger slips and cancels the wrong order), a queue of operations when the kitchen is slammed, a log of who did what, and shortcuts that fire several actions at once. Command solves all of that with one move: turn each request into an object — and once the request is an object, it can be stored, queued, reverted, and composed.

Contents

  1. The problem in PideYa: actions that evaporate
  2. Intent and structure of the pattern
  3. Complete Java implementation
  4. Undo: the command history
  5. Command queues and macro commands
  6. Command in modern Java: lambdas and Runnable
  7. When to use it and when not to
  8. Relationship with other patterns
  9. Common mistakes
  10. Exercises and conclusion

The problem in PideYa: actions that evaporate

The current version of the panel couples each button to the logic it runs:

public class ManagementPanelUI {
    public void onClickAccept(Order order) {
        order.accept();
        notifier.send("Your order has been accepted");
    }
    public void onClickCancel(Order order) {
        order.cancel();
        gateway.refund(order);
        notifier.send("Your order has been cancelled");
    }
    // ... one method per button, with no memory of what was done
}

What this design cannot do:

  • Undo: once the cancellation runs, no trace remains of what was done or how to revert it.
  • Queue: at rush hour the restaurant wants to accept orders and have them processed in order; a direct call runs now or never.
  • Log/audit: "who cancelled order 4412, and when?" requires scattering logging across every method.
  • Reuse the action from somewhere else (API, keyboard shortcut, an "accept everything under €20" automation): the logic is welded to the button.
  • Compose: the "close kitchen" shortcut (reject pending orders + notify delivery + pause the menu) has nowhere to live.

The common denominator: the request exists only as a call on the stack. What we need is to reify it — to make "cancel order 4412" a piece of data we can work with.

Intent and structure of the pattern

Intent (GoF): encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

Four roles, and the key is in who ignores whom: the Invoker (the button panel) executes commands without knowing what they do; the Receiver (the order, the gateway) does the work without knowing who asked for it; the Command joins them by storing everything needed to execute (and revert) the request.

classDiagram
    class Command {
        <<interface>>
        +execute()
        +undo()
        +description() String
    }
    class AcceptOrderCommand {
        -order: Order
        -notifier: Notifier
        +execute()
        +undo()
    }
    class CancelOrderCommand {
        -order: Order
        -gateway: PaymentGateway
        -notifier: Notifier
        +execute()
        +undo()
    }
    class ManagementPanel {
        -history: Deque~Command~
        +execute(c: Command)
        +undoLast()
    }
    class Order {
        +accept()
        +cancel()
        +reopen()
    }

    Command <|.. AcceptOrderCommand
    Command <|.. CancelOrderCommand
    ManagementPanel o-- Command : history
    AcceptOrderCommand --> Order : receiver
    CancelOrderCommand --> Order : receiver
GoF role In PideYa
Command (interface of the request) Command
ConcreteCommand AcceptOrderCommand, CancelOrderCommand, MarkPreparingCommand...
Invoker (fires commands, keeps the history) ManagementPanel
Receiver (who does the real work) Order, PaymentGateway, Notifier
Client (creates the command and sets its receiver) UI / configuration code
sequenceDiagram
    participant UI as Cancel button
    participant P as ManagementPanel (Invoker)
    participant C as CancelOrderCommand
    participant R as Order (Receiver)
    UI->>C: new CancelOrderCommand(order, ...)
    UI->>P: execute(command)
    P->>C: execute()
    C->>R: cancel()
    P->>P: history.push(command)
    Note over P: later...
    UI->>P: undoLast()
    P->>C: undo()
    C->>R: reopen()

Complete Java implementation

The Command interface. Deliberately small; description() feeds the audit trail and the UI label "Undo cancel order 4412":

public interface Command {
    void execute();
    void undo();
    String description();
}

A concrete command. Notice: it receives everything it needs in the constructor (receiver included) — the command is a frozen call, ready to fire at any time and place:

public class CancelOrderCommand implements Command {

    private final Order order;
    private final PaymentGateway gateway;
    private final Notifier customerNotifier;

    public CancelOrderCommand(Order order, PaymentGateway gateway,
                              Notifier customerNotifier) {
        this.order = order;
        this.gateway = gateway;
        this.customerNotifier = customerNotifier;
    }

    @Override
    public void execute() {
        order.cancel();
        gateway.refund(order.getId(), order.getTotal());
        customerNotifier.send("Your order " + order.getId() + " has been cancelled");
    }

    @Override
    public void undo() {
        order.reopen();
        gateway.charge(order.getId(), order.getTotal());
        customerNotifier.send("Cancellation reversed: your order " + order.getId() + " is back on track");
    }

    @Override
    public String description() {
        return "Cancel order " + order.getId();
    }
}

AcceptOrderCommand and MarkPreparingCommand follow the mold: execute() moves forward (order.accept(), order.markPreparing()) and undo() moves back (order.backToPending(), order.backToAccepted()). Which transitions are legal is not the command's business: State will govern that inside Order.

The Invoker. It doesn't know what any command does; it knows how to execute them, remember them, and audit them:

public class ManagementPanel {

    private final Deque<Command> history = new ArrayDeque<>();

    public void execute(Command command) {
        command.execute();
        history.push(command);
        EventLog.INSTANCE.info("Executed: " + command.description());
    }

    public void undoLast() {
        if (history.isEmpty()) {
            return;
        }
        Command last = history.pop();
        last.undo();
        EventLog.INSTANCE.info("Undone: " + last.description());
    }
}

The client assembles and fires:

panel.execute(new CancelOrderCommand(order, gateway, customerNotifier));
// oops, wrong order!
panel.undoLast();

The button panel, the public API, and the "accept small orders" automation now execute the same command objects: the action lives in a single place.

Undo: the command history

The Deque used as a stack is the essence of undo: LIFO — the last thing done is undone first. Three design decisions every real undo must make:

  1. Undo by inverse or by snapshot? Our undo() performs the inverse operation (reopen, re-charge). It works when the inverse exists and is reliable. When there is none (what's the inverse of "beating the eggs"?), the alternative is to save a snapshot of the previous state and restore it — that is exactly Memento, Command's classic partner, and we develop it there.
  2. Is everything undoable? No: an already-delivered order can't be "un-delivered". Options: have undo() throw NonReversibleOperationException, or a separate interface ReversibleCommand extends Command and have the panel stack only those.
  3. A bounded history? An unbounded Deque is a memory leak in a panel that runs for weeks. Bound it (e.g., 50 entries), discarding from the bottom.

For redo, add a second stack: undoLast() moves the command to redoStack; executing a new command clears it.

Command queues and macro commands

Queue. Because the command is data, executing "now" versus "when its turn comes" is just a matter of changing the invoker: a BlockingQueue<Command> where the button panel deposits and a worker thread drains. The saturated kitchen processes in order, losing nothing, and the producer doesn't wait. (This traveling command is the seed of the message buses and task queues we'll see in module 6.)

Macro command. A command whose children are commands — the Composite idea applied to actions:

public class MacroCommand implements Command {

    private final List<Command> steps;

    public MacroCommand(String name, List<Command> steps) { /* ... */ }

    @Override
    public void execute() {
        steps.forEach(Command::execute);
    }

    @Override
    public void undo() {
        // in reverse order! The last thing done is the first thing undone
        for (int i = steps.size() - 1; i >= 0; i--) {
            steps.get(i).undo();
        }
    }
}

The "close kitchen" shortcut is a MacroCommand with three steps — and the panel executes, stacks, and undoes it exactly like a simple command. A detail with teeth: if step 2 of 3 fails halfway through execute(), do you undo the ones already executed? That is a compensation, and it's the seed of the microservices Saga pattern (module 6).

Command in modern Java: lambdas and Runnable

Runnable is, literally, the Command interface with a single method and no undo: ExecutorService.submit(Runnable) is an invoker with a built-in command queue in the JDK. And if your command needs neither undo() nor description(), a functional interface and lambdas are enough:

@FunctionalInterface
public interface Action { void execute(); }

Map<String, Action> shortcuts = Map.of(
    "accept",    () -> order.accept(),
    "preparing", () -> order.markPreparing()
);
shortcuts.get("accept").execute();

It's the same spirit as the NotifierRegistry with Suppliers from Factory Method: the lambda captures the receiver and the arguments, just like the command's constructor. The practical rule: lambda for "fire and forget"; a class when the command has state or a contract (undo, description, auditing, serialization). Our panel needs all three: classes.

When to use it and when not to

Use it when:

  • You need undo/redo, a history, or an audit trail of operations.
  • You want to queue, delay, or schedule requests (execute at another time, on another thread, on another machine).
  • You want to parameterize objects with actions (buttons, menus, configurable shortcuts) or compose operations (macros).
  • You need to decouple who asks from who does and from when it gets done.

Avoid it when:

  • It's a direct, synchronous call with no history and no variability: a plain order.accept() is clearer than three classes around it.
  • "Reversibility" is illusory (irreversible external effects: emails sent, settled charges): an undo that doesn't truly undo is worse than none.

The price: one class per operation (mitigated with lambdas for the simple cases) and the discipline of keeping undo() a faithful mirror of execute() — every change to one must be reflected in the other.

Relationship with other patterns

  • Memento: undo by snapshot; Command+Memento is the classic duo of robust undo.
  • Composite: the MacroCommand is a composite of commands.
  • Chain of Responsibility: the chain's handlers can receive commands as the request.
  • Strategy: superficially similar (an object encapsulating code); different intent — Strategy encapsulates how something is done; Command, that something was requested. Head-to-head in the comparison.
  • Prototype: commands cloned as templates before tweaking parameters.

Common mistakes

  • Commands that do the work themselves instead of delegating to receivers: the command bloats into a "method wearing a class costume", impossible to reuse. The command coordinates; the receiver does.
  • undo() drifting out of sync with execute(): an effect gets added to execution (a notification, a charge) and nobody touches the inverse. Test them as a pair: execute(); undo(); must leave the system as it was.
  • An unbounded history: a silent memory leak (and with a Memento inside, a serious one).
  • Stacking failed commands: if execute() throws, the command must not enter the history (note that our ManagementPanel pushes after executing, precisely for this reason).
  • Undoing macros in the same order they executed: the inverses must be applied in reverse order, or the dependencies between steps break.

Exercises

Exercise 1: delay command

Implement NotifyDelayCommand, which adds minutes to the order's estimated time (order.delayDelivery(minutes)) and notifies the customer. Its undo() must subtract the delay and announce that the time is back to the original. What does the command store in order to undo?

Exercise 2: redo

Extend ManagementPanel with redoLast() using a second stack. Rule: executing a new command invalidates whatever was redoable. Write the complete class.

Exercise 3: command or lambda?

For each case, decide whether you would use a full command class or a lambda/Runnable, and why: (a) refreshing the order list when F5 is pressed; (b) cancelling an order with a payment refund; (c) the internal steps of the "close kitchen" macro; (d) retrying a notification delivery in the background.

Solutions

Solution 1: the command stores the minutes it applied (its parameter doubles as the undo information, because the inverse is "subtract what was added"):

public class NotifyDelayCommand implements Command {

    private final Order order;
    private final Notifier customerNotifier;
    private final int minutes;

    public NotifyDelayCommand(Order order, Notifier n, int minutes) {
        this.order = order;
        this.customerNotifier = n;
        this.minutes = minutes;
    }

    @Override public void execute() {
        order.delayDelivery(minutes);
        customerNotifier.send("Your order is delayed by " + minutes + " min. Sorry!");
    }

    @Override public void undo() {
        order.delayDelivery(-minutes);
        customerNotifier.send("Good news! Your order is back on its original schedule");
    }

    @Override public String description() {
        return "Delay order " + order.getId() + " (" + minutes + " min)";
    }
}

If the inverse weren't a simple -minutes (e.g., if delaying recalculates routes), you would have to store the previous estimated time — and as soon as the state to store grows, you're asking for Memento.

Solution 2:

public class ManagementPanel {

    private final Deque<Command> undoStack = new ArrayDeque<>();
    private final Deque<Command> redoStack = new ArrayDeque<>();

    public void execute(Command command) {
        command.execute();
        undoStack.push(command);
        redoStack.clear();               // anything new invalidates the redoable
    }

    public void undoLast() {
        if (undoStack.isEmpty()) return;
        Command c = undoStack.pop();
        c.undo();
        redoStack.push(c);
    }

    public void redoLast() {
        if (redoStack.isEmpty()) return;
        Command c = redoStack.pop();
        c.execute();
        undoStack.push(c);
    }
}

Solution 3: (a) lambda — no state, no undo, pure UI action; (b) class — it needs undo, a description for auditing, and it coordinates several receivers; (c) classes — the macro needs to undo them in reverse order, so they must honor the full Command contract; (d) Runnable in an executor — deferred "fire and forget"; the retry is already provided by the RetryNotifier from module 3, not the command.

Conclusion

Command reifies the request: "cancel order 4412" has stopped being a fleeting call and become an object with execute(), undo(), and description(), which the ManagementPanel fires, stacks, audits, and reverts without knowing what's inside. As a bonus: command queues for rush hour, macro commands for composed shortcuts, and the lightweight lambda version for actions with no contract. And the Memento seed got planted: when the inverse isn't enough, you save a snapshot.

The next pattern also turns something into objects, but something more exotic: sentences of a language. Marketing wants to write promotion rules like "total > 30 AND day == FRIDAY → 10% off" without waiting for a deployment. To evaluate them we need a grammar, an expression tree, and an interpreter — and we also need a frank conversation about why this pattern is the least used in the catalog. See you in Interpreter.

© Copyright 2026. All rights reserved