Skip to content

Low Level DesignDesign patterns

Command pattern

The Command design pattern through three real examples, from editor toolbars and shortcuts to undo and redo to a retry-safe job queue, with diagrams, live demos and code in five languages.

Mid-level20 min read

A command is a request turned into an object. Say the code wants to make text bold. Instead of calling document.bold() directly, it builds a BoldCommand. That object knows what to do, and to whom. The code hands it to whoever will run it. Because the request is now a value, it can be passed around and run from several places. It can be stored, undone and logged. It can even be put on a queue, and run later by another process.

You have used it without the name. Every action in VS Code’s command palette is a registered command. Swing’s Action is shared by a menu item and a toolbar button. WPF’s ICommand has Execute and CanExecute. Background job systems such as Celery put a task name and its arguments onto a queue. This page builds in three steps. First, one action shared by three triggers. Then undo and redo. Then commands that cross a network, and must survive crashes and retries.

The idea in one picture
toolbar [ B ] Format › Bold Ctrl + B invokers: they know nothing about bold run(cmd) BoldCommand execute() undo() + the data it needs calls Document does the real work it's an object, so you can keep it stack it → undo log it → audit queue it → later
Scroll or pinch to zoom, drag to pan
The toolbar, the menu and the shortcut each hold the same command object and only know how to run it. The document does the real work. Because the request is an object, it can also be stacked for undo, logged, or queued for later.

Every command setup has the same parts:

  • The command interface declares execute(), and often undo() or canExecute().
  • Concrete commands implement it. Each holds a reference to its receiver and the arguments it needs, for example DeleteRange(start, end) on a document.
  • The receiver does the real work, for example Document. It knows nothing about commands.
  • The invoker triggers commands without knowing what they do: a button, a key map, an undo history, a job worker.
  • The client builds concrete commands, connects them to receivers and gives them to invokers. It’s usually the startup code.
The general shape

creates

configures

holds

calls

Client

Invoker

-command: Command

+trigger()

«interface»

Command

+execute()

ConcreteCommand

-receiver: Receiver

-args

+execute()

Receiver

+action(args)

creates

configures

holds

calls

Client

Invoker

-command: Command

+trigger()

«interface»

Command

+execute()

ConcreteCommand

-receiver: Receiver

-args

+execute()

Receiver

+action(args)

Scroll or pinch to zoom, drag to pan
  • Color key
  • Facade
  • Entity
  • Value object
  • Enum
  • Interface
  • Implementation

The invoker has no arrow to the receiver. That gap is the pattern. The button doesn’t know a document exists. So the same button class works for any action, and the same action works from any button.

Easy

One action, three triggers

  • The basic shape
  • Invokers that share one action
  • One place to check and log

A notes app has a toolbar, a menu bar and keyboard shortcuts. Bold is offered in all three: the B button, Format › Bold, and Ctrl+B. So is Delete. All three must behave the same way. All three must be greyed out when nothing is selected. And the product team wants a log of which actions people use, and from where.

Each piece of UI edits the document itself:

Main.javashortcuts.pyProgram.csshortcuts.goshortcuts.ts
// The document the editor shows: some text and a selection [start, end).
class Document {
String text;
int start, end;
Document(String text) { this.text = text; }
void select(int start, int end) { this.start = start; this.end = end; }
String selected() { return text.substring(start, end); }
void replaceSelection(String s) {
text = text.substring(0, start) + s + text.substring(end);
end = start + s.length();
}
}
// Each piece of UI edits the document itself, so the logic for "bold" lives in three places.
class Toolbar {
private final Document doc;
Toolbar(Document doc) { this.doc = doc; }
void onBoldClick() {
if (doc.start == doc.end) return; // nothing selected
doc.replaceSelection("**" + doc.selected() + "**");
}
}
class Menu {
private final Document doc;
Menu(Document doc) { this.doc = doc; }
void onBold() {
// A copy of the toolbar's code. A fix to one copy has to be made to every copy.
if (doc.start == doc.end) return;
doc.replaceSelection("**" + doc.selected() + "**");
}
void onDelete() { doc.replaceSelection(""); }
}
class Shortcuts {
private final Document doc;
Shortcuts(Document doc) { this.doc = doc; }
void onKey(String keys) {
// Another copy, and this one forgot the empty-selection check.
if (keys.equals("Ctrl+B")) doc.replaceSelection("**" + doc.selected() + "**");
else if (keys.equals("Delete")) doc.replaceSelection("");
}
}
class Main {
public static void main(String[] args) {
Document doc = new Document("ship the release today");
Toolbar toolbar = new Toolbar(doc);
Menu menu = new Menu(doc);
Shortcuts keys = new Shortcuts(doc);
doc.select(9, 16);
toolbar.onBoldClick();
System.out.println(doc.text); // ship the **release** today
doc.select(20, 26);
keys.onKey("Delete");
System.out.println(doc.text); // ship the **release**
// The delete left nothing selected. The toolbar would do nothing here; the shortcut's copy
// inserts stray markers.
keys.onKey("Ctrl+B");
System.out.println(doc.text); // ship the **release******
// And there is no single place to log actions, grey out buttons, or add undo later.
}
}

The logic for bold is written three times. The keyboard’s copy was written later, and it missed the empty-selection check. So Ctrl+B with nothing selected inserts ****. There is no single place to grey out the toolbar and the menu together, or to log usage. And every new trigger (a right-click menu, a command palette) means another copy.

Make each action an object with execute() and canExecute(). Every invoker holds a command and runs it through one CommandRunner, which checks, executes and logs:

Easy: three invokers, one command per action

holds

holds

holds

run

run

run

ToolbarButton

+click()

MenuItem

+enabled() : bool

+click()

Keymap

-bindings: Map<keys, Command>

+press(keys)

CommandRunner

-log

+run(cmd, source)

«interface»

Command

+canExecute() : bool

+execute()

BoldCommand

DeleteCommand

Document

+text

+select(start, end)

+replaceSelection(s)

holds

holds

holds

run

run

run

ToolbarButton

+click()

MenuItem

+enabled() : bool

+click()

Keymap

-bindings: Map<keys, Command>

+press(keys)

CommandRunner

-log

+run(cmd, source)

«interface»

Command

+canExecute() : bool

+execute()

BoldCommand

DeleteCommand

Document

+text

+select(start, end)

+replaceSelection(s)

Scroll or pinch to zoom, drag to pan
  • Color key
  • Facade
  • Entity
  • Value object
  • Enum
  • Interface
  • Implementation

Select a word, then use the toolbar, the menu or the keyboard. Switch to the other design and press Ctrl+B with nothing selected:

Toolbar
Menu
Keyboard

Click a word to select it; shift-click to extend. While the text has focus, Ctrl+B (⌘B) and Delete work too.

Command objects

  • BoldCommandused by B, Format › Bold, Ctrl+B0 runs
  • DeleteCommandused by Delete, Edit › Delete, Delete key0 runs

Runner log

    Click "release" to select it, then make it bold from any of the three places.

    Main.javakeymap.pyProgram.cskeymap.gokeymap.ts
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    // The receiver: the object that knows how to change the text. It knows nothing about buttons.
    class Document {
    String text;
    int start, end;
    Document(String text) { this.text = text; }
    void select(int start, int end) { this.start = start; this.end = end; }
    String selected() { return text.substring(start, end); }
    void replaceSelection(String s) {
    text = text.substring(0, start) + s + text.substring(end);
    end = start + s.length();
    }
    }
    // One object per action. Whoever triggers it only needs this interface.
    interface Command {
    String name();
    // Every invoker asks the same question, so buttons, menus and keys enable together.
    boolean canExecute();
    void execute();
    }
    class BoldCommand implements Command {
    private final Document doc;
    BoldCommand(Document doc) { this.doc = doc; }
    public String name() { return "bold"; }
    public boolean canExecute() { return doc.start < doc.end; }
    public void execute() { doc.replaceSelection("**" + doc.selected() + "**"); }
    }
    class DeleteCommand implements Command {
    private final Document doc;
    DeleteCommand(Document doc) { this.doc = doc; }
    public String name() { return "delete"; }
    public boolean canExecute() { return doc.start < doc.end; }
    public void execute() { doc.replaceSelection(""); }
    }
    // Every invoker goes through one runner, so checking, logging (and later, undo) happen once.
    class CommandRunner {
    final List<String> log = new ArrayList<>();
    void run(Command cmd, String source) {
    if (!cmd.canExecute()) {
    System.out.println("[" + source + "] " + cmd.name() + " skipped: nothing selected");
    return;
    }
    cmd.execute();
    log.add(cmd.name() + " via " + source);
    System.out.println("[" + source + "] " + cmd.name());
    }
    }
    // Invokers hold a command and trigger it. None of them knows what bold means.
    class ToolbarButton {
    private final Command cmd;
    private final CommandRunner runner;
    ToolbarButton(Command cmd, CommandRunner runner) { this.cmd = cmd; this.runner = runner; }
    void click() { runner.run(cmd, "toolbar"); }
    }
    class MenuItem {
    final String label;
    private final Command cmd;
    private final CommandRunner runner;
    MenuItem(String label, Command cmd, CommandRunner runner) {
    this.label = label; this.cmd = cmd; this.runner = runner;
    }
    boolean enabled() { return cmd.canExecute(); }
    void click() { runner.run(cmd, "menu"); }
    }
    class Keymap {
    private final CommandRunner runner;
    private final Map<String, Command> bindings = new HashMap<>();
    Keymap(CommandRunner runner) { this.runner = runner; }
    void bind(String keys, Command cmd) { bindings.put(keys, cmd); }
    void press(String keys) {
    Command cmd = bindings.get(keys);
    if (cmd != null) runner.run(cmd, keys);
    }
    }
    class Main {
    public static void main(String[] args) {
    Document doc = new Document("ship the release today");
    CommandRunner runner = new CommandRunner();
    // One command object per action, shared by every invoker that offers it.
    Command bold = new BoldCommand(doc), delete = new DeleteCommand(doc);
    ToolbarButton boldButton = new ToolbarButton(bold, runner);
    MenuItem formatBold = new MenuItem("Format > Bold", bold, runner);
    MenuItem editDelete = new MenuItem("Edit > Delete", delete, runner);
    Keymap keys = new Keymap(runner);
    keys.bind("Ctrl+B", bold);
    keys.bind("Delete", delete);
    doc.select(9, 16);
    boldButton.click(); // [toolbar] bold
    System.out.println(doc.text); // ship the **release** today
    doc.select(20, 26);
    keys.press("Delete"); // [Delete] delete
    System.out.println(doc.text); // ship the **release**
    // Nothing is selected now. The shortcut asks the same command, so it can't insert stray markers.
    keys.press("Ctrl+B"); // [Ctrl+B] bold skipped: nothing selected
    String state = editDelete.enabled() ? "enabled" : "greyed out";
    System.out.println(editDelete.label + " is " + state); // Edit > Delete is greyed out
    doc.select(0, 4);
    formatBold.click(); // [menu] bold
    System.out.println(doc.text); // **ship** the **release**
    System.out.println("log: " + String.join(", ", runner.log)); // log: bold via toolbar, delete via Delete, bold via menu
    }
    }
    • One object per action, shared. The toolbar’s B, Format › Bold and Ctrl+B hold the same BoldCommand. A fix to bold is made once, and every trigger gets it.
    • Invokers are generic. ToolbarButton and MenuItem don’t know what bold is. A command palette or a right-click menu is just a new invoker over the existing commands. A new action is a new command, and every invoker can offer it.
    • canExecute keeps the UI consistent. Every invoker asks the command. So the toolbar and the menu grey out together, and the shortcut can’t do something the button wouldn’t. WPF’s ICommand and Swing’s Action (with isEnabled) are built around the same idea.
    • The runner is a single choke point. Checking, logging and timing happen once, for every action from every source. In the Medium level this is where undo history goes.
    • The receiver stays simple. Document knows how to replace a selection. It doesn’t know about menus, shortcuts or logs, which is single responsibility applied to UI actions.
    Interviewer

    Our editor has bold in the toolbar, the menu and Ctrl+B, and they keep drifting apart. Now we’re adding a command palette. How would you structure the actions?

    What they are testing

    Whether you make each action an object with execute and canExecute, shared by every invoker and run through one place that can log. Copying the logic into a fourth handler, or a giant switch (actionName) that each UI element calls, is the red flag.

    Say this out loud

    I’ll make each action a command object, with execute and canExecute. The toolbar, the menu, the shortcuts and the palette are invokers. Each holds a command, and runs it through one runner. The document only knows how to edit text. A new trigger is a new invoker. A new action is a new command.

    Medium

    Undo and redo

    • undo() next to execute()
    • Undo and redo stacks
    • Merging keystrokes into one step
    • Commands vs. snapshots (Memento)

    The same editor now needs undo and redo. Users expect three things from any editor they’ve used. Undo takes back a word they typed, not one letter. Redo works until they make a new change. And undo of a delete brings back exactly what was deleted.

    Record the name of each action, and reverse it in one big method:

    Editor.javaeditor.pyEditor.cseditor.goeditor.ts
    import java.util.ArrayList;
    import java.util.List;
    // Undo without command objects: the editor records the NAME of each action, and one big
    // method tries to work out how to reverse it after the fact.
    class Editor {
    String text = "";
    final List<Object[]> history = new ArrayList<>(); // {action, position, length}
    void insert(int pos, String s) {
    text = text.substring(0, pos) + s + text.substring(pos);
    history.add(new Object[] {"insert", pos, s.length()});
    }
    void delete(int start, int end) {
    text = text.substring(0, start) + text.substring(end);
    // Only the position and length are recorded. The removed text is gone.
    history.add(new Object[] {"delete", start, end - start});
    }
    void undo() {
    Object[] last = history.remove(history.size() - 1);
    String action = (String) last[0];
    int pos = (int) last[1], n = (int) last[2];
    // How to reverse an action lives here, far from the code that performed it. Every
    // new action needs a new branch, and each branch needs data nobody kept.
    if (action.equals("insert")) {
    text = text.substring(0, pos) + text.substring(pos + n);
    } else if (action.equals("delete")) {
    System.out.println("cannot undo delete: the removed text was never saved");
    }
    }
    }
    class Main {
    public static void main(String[] args) {
    Editor ed = new Editor();
    for (char ch : "Hello".toCharArray()) {
    ed.insert(ed.text.length(), String.valueOf(ch)); // one history entry per keystroke
    }
    // Undoing the typing takes back one letter at a time, and there is no redo at all.
    ed.undo();
    System.out.println("\"" + ed.text + "\" with " + ed.history.size() + " steps left"); // "Hell" with 4 steps left
    ed.delete(0, 4);
    ed.undo(); // cannot undo delete: the removed text was never saved
    System.out.println("\"" + ed.text + "\""); // ""
    }
    }

    Undo lives far from the code that performs each action, so every new action needs a new branch in undo. Worse, the delete didn’t keep the text it removed, so there is nothing to put back. Typing is one step per letter, and there is no redo.

    Give every command an undo() next to its execute(), and let it keep what it needs to reverse itself. A History invoker holds two stacks: commands that can be undone, and commands that were just undone:

    Medium: commands that undo themselves, and a history with two stacks

    two stacks

    *

    History

    -undoStack: Command[]

    -redoStack: Command[]

    -limit

    +run(cmd)

    +undo()

    +redo()

    «interface»

    Command

    +execute(doc)

    +undo(doc)

    +merge(next) : bool

    InsertText

    -pos

    -text

    -lastMs

    DeleteRange

    -start, end

    -removed

    Document

    +insert(pos, s)

    +delete(start, end) : removed

    two stacks

    *

    History

    -undoStack: Command[]

    -redoStack: Command[]

    -limit

    +run(cmd)

    +undo()

    +redo()

    «interface»

    Command

    +execute(doc)

    +undo(doc)

    +merge(next) : bool

    InsertText

    -pos

    -text

    -lastMs

    DeleteRange

    -start, end

    -removed

    Document

    +insert(pos, s)

    +delete(start, end) : removed

    Scroll or pinch to zoom, drag to pan
    • Color key
    • Facade
    • Entity
    • Value object
    • Enum
    • Interface
    • Implementation

    The rules are short:

    • Run: execute the command, push it on the undo stack (or merge it into the top one), and clear the redo stack.
    • Undo: pop from the undo stack, call undo(), push onto the redo stack.
    • Redo: pop from the redo stack, call execute() again, push back onto the undo stack.

    Type into the box or use the buttons, then undo and redo. Undo something and type again to see the redo stack cleared. Turn merging off to see one step per key:

    Undo stack (0)

      Redo stack (0)

        Click into the box and type, or use the buttons. Then undo and redo.

        History.javahistory.pyHistory.cshistory.gohistory.ts
        import java.util.ArrayDeque;
        import java.util.ArrayList;
        import java.util.Deque;
        import java.util.Iterator;
        import java.util.List;
        // The receiver. It only knows how to insert and delete; it has no idea undo exists.
        class Document {
        String text = "";
        void insert(int pos, String s) { text = text.substring(0, pos) + s + text.substring(pos); }
        String delete(int start, int end) {
        String removed = text.substring(start, end);
        text = text.substring(0, start) + text.substring(end);
        return removed;
        }
        }
        // Each command knows how to do itself AND how to take itself back, so reversing an action
        // lives next to the code that performs it.
        interface Command {
        void execute(Document doc);
        void undo(Document doc);
        // Asked by the history: "can you absorb this next command into yourself?"
        boolean merge(Command next);
        String label();
        }
        class InsertText implements Command {
        static final int MERGE_WINDOW_MS = 1000;
        final int pos;
        String text;
        long lastMs;
        InsertText(int pos, String text, long atMs) { this.pos = pos; this.text = text; this.lastMs = atMs; }
        public void execute(Document doc) { doc.insert(pos, text); }
        public void undo(Document doc) { doc.delete(pos, pos + text.length()); }
        public boolean merge(Command next) {
        // Typing is many keystrokes but one undo step: absorb the next insert if it continues
        // right where this one ended, within a second of the last key.
        if (next instanceof InsertText n
        && n.pos == pos + text.length()
        && n.lastMs - lastMs <= MERGE_WINDOW_MS) {
        text += n.text;
        lastMs = n.lastMs;
        return true;
        }
        return false;
        }
        public String label() { return "insert \"" + text + "\""; }
        }
        class DeleteRange implements Command {
        private final int start, end;
        private String removed = "";
        DeleteRange(int start, int end) { this.start = start; this.end = end; }
        public void execute(Document doc) {
        // Capture what undo will need at the moment of execution, not at construction:
        // only now do we know what the range holds.
        removed = doc.delete(start, end);
        }
        public void undo(Document doc) { doc.insert(start, removed); }
        public boolean merge(Command next) { return false; }
        public String label() { return "delete \"" + removed + "\""; }
        }
        // The invoker that remembers. Two stacks: what can be undone, and what was just undone.
        class History {
        private final Document doc;
        private final int limit;
        private final Deque<Command> undoStack = new ArrayDeque<>();
        private final Deque<Command> redoStack = new ArrayDeque<>();
        // After an undo or redo, the next keystroke starts a new step instead of merging.
        private boolean sealed = true;
        History(Document doc, int limit) { this.doc = doc; this.limit = limit; }
        void run(Command cmd) {
        cmd.execute(doc);
        if (undoStack.isEmpty() || sealed || !undoStack.peek().merge(cmd)) {
        undoStack.push(cmd);
        if (undoStack.size() > limit) undoStack.removeLast(); // memory is bounded: the oldest step is forgotten
        }
        sealed = false;
        // A new action starts a new timeline. The undone future no longer applies to this
        // text, so redoing it would corrupt the document.
        redoStack.clear();
        }
        void undo() {
        if (undoStack.isEmpty()) return;
        Command cmd = undoStack.pop();
        cmd.undo(doc);
        redoStack.push(cmd);
        sealed = true;
        }
        void redo() {
        if (redoStack.isEmpty()) return;
        Command cmd = redoStack.pop();
        cmd.execute(doc);
        undoStack.push(cmd);
        sealed = true;
        }
        void show() {
        // Stacks print bottom to top: the last item is the next to be undone or redone.
        System.out.println("\"" + doc.text + "\" | undo: [" + labels(undoStack) + "] | redo: [" + labels(redoStack) + "]");
        }
        private static String labels(Deque<Command> stack) {
        List<String> out = new ArrayList<>();
        for (Iterator<Command> it = stack.descendingIterator(); it.hasNext(); ) out.add(it.next().label());
        return String.join(", ", out);
        }
        }
        class Main {
        static Document doc = new Document();
        static History history = new History(doc, 100);
        // One InsertText per keystroke, 100 ms apart, the way a keyboard delivers them.
        static void typeText(String s, long atMs) {
        for (int i = 0; i < s.length(); i++) {
        history.run(new InsertText(doc.text.length(), String.valueOf(s.charAt(i)), atMs + i * 100L));
        }
        }
        public static void main(String[] args) {
        typeText("Hello", 0);
        typeText(" world", 2000); // after a 1.6 s pause, so a new undo step
        history.show(); // "Hello world" | undo: [insert "Hello", insert " world"] | redo: []
        history.undo();
        history.undo();
        history.show(); // "" | undo: [] | redo: [insert " world", insert "Hello"]
        history.redo();
        history.show(); // "Hello" | undo: [insert "Hello"] | redo: [insert " world"]
        // A new edit after undo throws the redo stack away.
        typeText("!", 5000);
        history.show(); // "Hello!" | undo: [insert "Hello", insert "!"] | redo: []
        history.run(new DeleteRange(0, 5));
        history.show(); // "!" | undo: [insert "Hello", insert "!", delete "Hello"] | redo: []
        history.undo();
        history.show(); // "Hello!" | undo: [insert "Hello", insert "!"] | redo: [delete "Hello"]
        }
        }
        • A command keeps what it needs, captured when it runs. DeleteRange saves the removed text in execute, not in its constructor. Only when it runs does it know what the range holds. That one field is what makes delete undoable.
        • A new edit clears the redo stack. The undone commands were recorded against a text that no longer exists. Say the user typed “!” at position 5. Replaying “ world” at position 5 could now insert it in the wrong place. So editors drop that branch of history.
        • Typing merges. Eleven keystrokes become two undo steps. InsertText.merge absorbs the next insert when it continues at the same position less than a second later. Java Swing’s UndoableEdit.addEdit gives an edit the same chance to absorb the next one. Real editors also break steps at word boundaries or when the cursor jumps.
        • Undo and redo seal the top step. After an undo or redo, the next keystroke starts a new step even if it is quick. Otherwise it could merge into a command the user just redid.
        • The history is bounded. After limit steps the oldest is dropped. Without a limit, an editor left open all day holds every keystroke since morning.
        • Receivers stay ignorant. Document has insert and delete and no idea that undo exists.

        There is another way to undo. Save a copy of the whole state before each step, and restore it on undo. That is the Memento pattern. It needs no inverse logic, so it can’t get an inverse wrong. It also works for actions that are hard to reverse, such as an image filter. The cost is memory, because each step stores the whole document. A 1 MB document with 100 undo steps holds about 100 MB of snapshots. The commands above hold only the characters that changed. The demo’s meter shows the gap growing as the text gets longer.

        Real tools often combine the two. They use commands for most steps. They take a snapshot every N steps, or before an operation that is expensive to reverse. Or they use persistent data structures, which share the unchanged parts between versions.

        Interviewer

        Add undo and redo to this text editor. Users complain that undo takes back one letter at a time, and that undoing a delete doesn’t bring the text back.

        What they are testing

        Whether each command stores what it needs to reverse itself (the deleted text, captured on execute). Whether you use two stacks, and clear redo on a new action. And whether you merge consecutive typing. Knowing the snapshot alternative, and its memory cost, is the bar for mid-level. The red flag is reversing actions in one big switch. So is saving a full copy of the document on every keystroke, without noticing the cost.

        Hard

        A retry-safe job queue

        • Commands as serialized data
        • Versioned payloads
        • Idempotency keys
        • Retries, backoff and a dead-letter queue
        • An audit log

        A shop’s checkout must not wait for slow work. When an order is paid, the web server enqueues SendInvoice{orderId}. When a product photo is uploaded, it enqueues ResizeImage{image, width}. Workers on other machines pick the jobs up. But workers crash and get redeployed. The email API times out. Some photos are corrupt. And the web server sometimes enqueues the same job twice, when its own request times out and is retried. Customers must get exactly one invoice per order. And support must be able to answer “was this invoice sent, and when?”

        These are still commands: a request, captured as a value, run later by someone else. But they now cross a process boundary, and that changes what the pattern has to handle.

        Code can’t be put on a queue. So a command that crosses a boundary is a message: a type name, a version, an idempotency key and a payload. The execute method moves to a handler on the worker, looked up by type. CQRS frameworks use the same split, and call it a command and its command handler.

        {"currency":"USD","key":"invoice:1001","orderId":"1001","type":"SendInvoice","v":"2"}
        • Send ids, not objects. The payload carries orderId, not the order. The worker reads the current order when it runs. That keeps messages small, and avoids acting on a stale copy. Sometimes the job must use the values as they were at enqueue time, like the amount the customer agreed to. Then put those values in the payload on purpose.
        • Version every payload. After a deploy, jobs enqueued by the previous release are still on the queue. The worker upgrades old versions as it reads them. (This is called “upcasting”.) SendInvoice v1 had no currency, and every v1 order was in euros. So v1 is read as v2 with EUR. Add fields with defaults. Never change what an existing field means.
        • Deploy consumers first. A producer that sends v3 before any worker understands it creates jobs nobody can run. Retrying won’t help, so they go straight to the dead-letter queue, to be replayed once the new workers are out.

        A queue can’t know whether a worker that went silent finished the job. So queues such as Amazon SQS don’t delete a message when it’s received. They hide it for a while, called a lease. (SQS calls it the visibility timeout.) When the worker is done, it deletes the message with an ack. If the worker crashes first, the lease runs out, and the job is delivered again. So every handler may run twice, and every handler must be safe to run twice.

        Try it below: enqueue an invoice, tick “crash”, and watch the job come back after its lease. Then turn idempotency keys off and do it again:

        t = 0 s

        Queue

          Mailer: invoices sent

            Dead-letter queue

              Audit log

                Enqueue a SendInvoice, tick "crash", and watch what happens when the job comes back.

                Hard: serialized commands, a queue with leases, and a worker with handlers

                enqueue

                receive, ack, retry

                by type

                *

                parsed from wire

                parsed from wire

                Producer

                +enqueue(type, v, key, payload)

                Queue

                -jobs: Job[]

                +receive(now) : Job

                +ack(job)

                +retryAt(job, at)

                Job

                +id

                +wire

                +visibleAt

                +attempts

                Worker

                -handlers: Map<type, Handler>

                -done: Set<key>

                -dead: Job[]

                +poll(now)

                «interface»

                Handler

                +handle(cmd, key)

                SendInvoice

                +orderId

                +currency

                ResizeImage

                +image

                +width

                Mailer

                +sendInvoice(key, cmd)

                ImageService

                +resize(cmd)

                enqueue

                receive, ack, retry

                by type

                *

                parsed from wire

                parsed from wire

                Producer

                +enqueue(type, v, key, payload)

                Queue

                -jobs: Job[]

                +receive(now) : Job

                +ack(job)

                +retryAt(job, at)

                Job

                +id

                +wire

                +visibleAt

                +attempts

                Worker

                -handlers: Map<type, Handler>

                -done: Set<key>

                -dead: Job[]

                +poll(now)

                «interface»

                Handler

                +handle(cmd, key)

                SendInvoice

                +orderId

                +currency

                ResizeImage

                +image

                +width

                Mailer

                +sendInvoice(key, cmd)

                ImageService

                +resize(cmd)

                Scroll or pinch to zoom, drag to pan
                • Color key
                • Facade
                • Entity
                • Value object
                • Enum
                • Interface
                • Implementation
                The worker dies between the send and the ack
                MailerWorkerQueueMailerWorkerQueuecrashes before ack, key not recordedt=30s: lease expired, j1 visible againj1 SendInvoice (lease until t=30s)send(invoice 1001, key invoice:1001)sentj1 again (attempt 2)send(invoice 1001, key invoice:1001)duplicate key, not sent againack j1MailerWorkerQueueMailerWorkerQueuecrashes before ack, key not recordedt=30s: lease expired, j1 visible againj1 SendInvoice (lease until t=30s)send(invoice 1001, key invoice:1001)sentj1 again (attempt 2)send(invoice 1001, key invoice:1001)duplicate key, not sent againack j1
                Scroll or pinch to zoom, drag to pan
                • Color key
                • Entity
                • Facade
                Worker.javaworker.pyWorker.csworker.goworker.ts
                import java.util.ArrayList;
                import java.util.HashMap;
                import java.util.HashSet;
                import java.util.List;
                import java.util.Map;
                import java.util.Set;
                import java.util.TreeMap;
                import java.util.regex.Matcher;
                import java.util.regex.Pattern;
                // The wire format. Once a command crosses a process boundary it is data, not an object with
                // an execute method: code can't be put on a queue, so the behavior moves to a handler.
                // Real code uses a JSON library such as Jackson; this keeps the example dependency-free.
                final class Wire {
                private static final Pattern FIELD = Pattern.compile("\"(\\w+)\":\"([^\"]*)\"");
                // Sorted keys and string values: the same command always encodes to the same text.
                static String encode(Map<String, String> fields) {
                List<String> parts = new ArrayList<>();
                new TreeMap<>(fields).forEach((k, v) -> parts.add("\"" + k + "\":\"" + v + "\""));
                return "{" + String.join(",", parts) + "}";
                }
                static Map<String, String> decode(String wire) {
                Map<String, String> fields = new HashMap<>();
                Matcher m = FIELD.matcher(wire);
                while (m.find()) fields.put(m.group(1), m.group(2));
                return fields;
                }
                }
                // Typed commands: what handlers work with after the payload is checked and upgraded.
                record SendInvoice(String orderId, String currency) {}
                record ResizeImage(String image, int width) {}
                // Retrying can't fix it: unknown type or version, or invalid data.
                class PermanentException extends RuntimeException {
                PermanentException(String message) { super(message); }
                }
                final class Commands {
                // Old versions are upgraded as they are read ("upcasting"), so jobs enqueued by the
                // previous release still run after a deploy. Unknown versions fail permanently.
                static Object parse(Map<String, String> f) {
                String kind = f.get("type"), v = f.get("v");
                if (kind.equals("SendInvoice") && v.equals("1")) {
                return new SendInvoice(f.get("orderId"), "EUR"); // v1 had no currency; every v1 order was in euros
                }
                if (kind.equals("SendInvoice") && v.equals("2")) return new SendInvoice(f.get("orderId"), f.get("currency"));
                if (kind.equals("ResizeImage") && v.equals("1")) {
                return new ResizeImage(f.get("image"), Integer.parseInt(f.get("width")));
                }
                throw new PermanentException("unknown command " + kind + " v" + v);
                }
                static void log(int now, String line) { System.out.println("t=" + now + "s " + line); }
                }
                // A fake email API. Like many payment and email APIs, it takes an idempotency key and
                // ignores a repeat, so a retried job can't send the same invoice twice.
                class Mailer {
                final Map<String, String> sent = new HashMap<>();
                void sendInvoice(String key, SendInvoice cmd, int now) {
                if (sent.containsKey(key)) {
                Commands.log(now, "mailer: duplicate key " + key + ", not sent again");
                return;
                }
                sent.put(key, cmd.orderId());
                Commands.log(now, "mailer: invoice " + cmd.orderId() + " sent (" + cmd.currency() + ")");
                }
                }
                // A fake image service: busy on the first call, and it always chokes on one corrupt file.
                // Resizing is idempotent by nature (same input, same output file), so it needs no key.
                class ImageService {
                private final Map<String, Integer> calls = new HashMap<>();
                void resize(ResizeImage cmd, int now) {
                int n = calls.merge(cmd.image(), 1, Integer::sum);
                if (cmd.image().equals("img/bad.png")) throw new RuntimeException("corrupt image");
                if (n == 1) throw new RuntimeException("image service 503");
                Commands.log(now, "image: " + cmd.image() + " resized to " + cmd.width() + "px");
                }
                }
                class Job {
                final String id, wire;
                int visibleAt, attempts;
                Job(String id, String wire) { this.id = id; this.wire = wire; }
                }
                class JobQueue {
                static final int LEASE_S = 30; // a delivered job is hidden this long; if nobody acks it, it is delivered again
                final List<Job> jobs = new ArrayList<>();
                void enqueue(String id, Map<String, String> fields) { jobs.add(new Job(id, Wire.encode(fields))); }
                Job receive(int now) {
                // At-least-once delivery: a received job is hidden for a lease, not removed. If the
                // worker dies before acking, the job comes back. So every handler may run twice.
                for (Job job : jobs) {
                if (job.visibleAt <= now) {
                job.visibleAt = now + LEASE_S;
                job.attempts++;
                return job;
                }
                }
                return null;
                }
                void ack(Job job) { jobs.remove(job); }
                void retryAt(Job job, int at) { job.visibleAt = at; }
                }
                interface Handler { void handle(Object cmd, String key, int now); }
                class Worker {
                static final int MAX_ATTEMPTS = 3;
                static final int BACKOFF_BASE_S = 2; // waits 2 s, then 4 s; real systems add random jitter
                private final JobQueue queue;
                private final Map<String, Handler> handlers;
                private final Set<String> done = new HashSet<>(); // idempotency keys of finished jobs (a table in real life)
                final List<String> dead = new ArrayList<>(); // the dead-letter queue: jobs parked for a person to look at
                final Set<String> crashAfter = new HashSet<>(); // simulation only: jobs whose worker dies before ack
                Worker(JobQueue queue, Map<String, Handler> handlers) { this.queue = queue; this.handlers = handlers; }
                // Every attempt and its outcome, in order. In production: an append-only table.
                private void audit(int now, Job job, String outcome) {
                Commands.log(now, job.id + " attempt " + job.attempts + ": " + outcome);
                }
                boolean poll(int now) {
                Job job = queue.receive(now);
                if (job == null) return false;
                Map<String, String> f = Wire.decode(job.wire);
                String key = f.get("key");
                if (done.contains(key)) {
                // Same key as a finished job, e.g. the producer enqueued it twice. Ack, don't rerun.
                queue.ack(job);
                audit(now, job, "skipped, key " + key + " already done");
                return true;
                }
                try {
                Object cmd = Commands.parse(f);
                handlers.get(f.get("type")).handle(cmd, key, now);
                } catch (PermanentException e) {
                // Retrying can't help, so park it at once instead of burning attempts.
                queue.ack(job);
                dead.add(job.id);
                audit(now, job, "dead-lettered (" + e.getMessage() + ")");
                return true;
                } catch (RuntimeException e) {
                if (job.attempts >= MAX_ATTEMPTS) {
                // A poison job: it fails every time. Park it so it stops blocking the queue.
                queue.ack(job);
                dead.add(job.id);
                audit(now, job, "dead-lettered after " + MAX_ATTEMPTS + " attempts (" + e.getMessage() + ")");
                } else {
                int delay = BACKOFF_BASE_S * (1 << (job.attempts - 1));
                queue.retryAt(job, now + delay);
                audit(now, job, "failed (" + e.getMessage() + "), retry in " + delay + "s");
                }
                return true;
                }
                if (crashAfter.remove(job.id)) {
                // Simulation: the process dies between the side effect and the ack. Nothing below
                // runs, so the key isn't recorded, and the job is delivered again after its lease.
                audit(now, job, "worker crashed before ack");
                return true;
                }
                done.add(key);
                queue.ack(job);
                audit(now, job, "done");
                return true;
                }
                }
                class Main {
                public static void main(String[] args) {
                Mailer mailer = new Mailer();
                ImageService images = new ImageService();
                JobQueue queue = new JobQueue();
                Worker worker = new Worker(queue, Map.of(
                "SendInvoice", (cmd, key, now) -> mailer.sendInvoice(key, (SendInvoice) cmd, now),
                "ResizeImage", (cmd, key, now) -> images.resize((ResizeImage) cmd, now)));
                // The idempotency key comes from what makes the request unique (one invoice per order), not
                // from a random id per enqueue, so a duplicate enqueue carries the same key.
                queue.enqueue("j1", Map.of("type", "SendInvoice", "v", "2", "key", "invoice:1001", "orderId", "1001", "currency", "USD"));
                queue.enqueue("j2", Map.of("type", "ResizeImage", "v", "1", "key", "resize:img/42.jpg:640", "image", "img/42.jpg", "width", "640"));
                queue.enqueue("j3", Map.of("type", "SendInvoice", "v", "1", "key", "invoice:1002", "orderId", "1002")); // from the old release
                queue.enqueue("j4", Map.of("type", "SendInvoice", "v", "1", "key", "invoice:1002", "orderId", "1002")); // enqueued twice
                queue.enqueue("j5", Map.of("type", "ResizeImage", "v", "1", "key", "resize:img/bad.png:640", "image", "img/bad.png", "width", "640"));
                queue.enqueue("j6", Map.of("type", "SendInvoice", "v", "3", "key", "invoice:1003", "orderId", "1003", "currency", "USD")); // a producer deployed too early
                System.out.println("wire j1: " + queue.jobs.get(0).wire); // wire j1: {"currency":"USD","key":"invoice:1001","orderId":"1001","type":"SendInvoice","v":"2"}
                worker.crashAfter.add("j1");
                for (int now = 0; now < 40; now++) {
                while (worker.poll(now)) { }
                }
                // t=0s mailer: invoice 1001 sent (USD)
                // t=0s j1 attempt 1: worker crashed before ack
                // t=0s j2 attempt 1: failed (image service 503), retry in 2s
                // t=0s mailer: invoice 1002 sent (EUR)
                // t=0s j3 attempt 1: done
                // t=0s j4 attempt 1: skipped, key invoice:1002 already done
                // t=0s j5 attempt 1: failed (corrupt image), retry in 2s
                // t=0s j6 attempt 1: dead-lettered (unknown command SendInvoice v3)
                // t=2s image: img/42.jpg resized to 640px
                // t=2s j2 attempt 2: done
                // t=2s j5 attempt 2: failed (corrupt image), retry in 4s
                // t=6s j5 attempt 3: dead-lettered after 3 attempts (corrupt image)
                // t=30s mailer: duplicate key invoice:1001, not sent again
                // t=30s j1 attempt 2: done
                System.out.println("emails sent: " + mailer.sent.size()); // emails sent: 2
                System.out.println("dead letters: " + String.join(", ", worker.dead)); // dead letters: j6, j5
                }
                }
                • The key comes from the business, not from the enqueue. invoice:1001 means “the invoice for order 1001”. A random id per enqueue would give the duplicate j4 a new key, and the customer two emails.
                • Two layers of idempotency. The worker records finished keys, and skips a job whose key is done (j4). That doesn’t cover a crash between the side effect and recording the key (j1). So the key also goes to the side effect itself. Many payment and email APIs accept one for this reason. Stripe’s Idempotency-Key header is a well-known example. When the side effect is a write to your own database, record the key in the same transaction as the write.
                • Some commands are idempotent by nature. Resizing an image to the same output file twice gives the same result. So ResizeImage needs no key at the side effect. Sending, charging and incrementing do.
                • Retry what might succeed. Park what can’t. An unknown version is a permanent error, so j6 goes to the dead-letter queue at once. Other errors are retried with exponential backoff (2 s, then 4 s). Real systems add random jitter, so a thousand failed jobs don’t all retry in the same second.
                • A poison job is capped, not retried forever. The corrupt image fails every attempt. After 3 it moves to the dead-letter queue, which should alert someone and let them replay the job once the cause is fixed. SQS (a redrive policy with a maximum receive count) and RabbitMQ (dead-letter exchanges) have this built in.
                • The audit log is every attempt, in order. Who enqueued what, each attempt, each outcome. It’s an append-only record, separate from the queue, and it’s how support answers “did the customer get the invoice?” weeks later.
                • The lease must outlast the job. Say a job takes longer than its lease. The queue delivers it to a second worker, while the first is still running. Both may pass the “is this key done?” check at once. So extend the lease while working (SQS’s ChangeMessageVisibility). And make recording the key an atomic claim, such as an insert that fails if the key exists.
                • Retries reorder jobs. j2 finishes after j3 although it was enqueued first. Handlers must not assume they see jobs in enqueue order.
                Interviewer

                After a deploy, some customers got their invoice email twice. The send happens in a background job. What happened, and how do you make sure it can’t happen again?

                What they are testing

                Whether you name at-least-once delivery. (The worker was stopped after sending, but before acking, so the job was delivered again.) Whether you fix it with an idempotency key derived from the order, checked by the worker and passed to the email provider. And whether you add a bounded retry policy and a dead-letter queue. The red flag is “make the queue exactly-once”, or “add a sleep before the ack”.

                Say this out loud

                Each job is a serialized command: a type, a version, an idempotency key, and a payload of ids. Delivery is at least once, so handlers are idempotent. The key is derived from the order. It’s recorded when the job finishes, and passed to the email API. Temporary errors retry with exponential backoff and jitter. Permanent errors, and jobs that fail three times, go to a dead-letter queue. Every attempt goes to an audit log.

                Use a command when the request must outlive the call. That’s when it’s undone, logged, queued, retried, scheduled or sent to another process. Or when many triggers must share one action. Call the method directly, or pass a function, when the action runs once, right away, and nothing needs to remember it.

                Language Usual form Built in or common
                Java An interface with execute(), or Runnable and Callable when no undo or data is needed Swing’s Action and javax.swing.undo (UndoableEdit, UndoManager), ExecutorService takes Runnable and Callable
                Python A class with execute and undo, or any callable (functools.partial binds arguments) Celery tasks: task.delay(...) serializes the task name and arguments onto a queue
                C# An interface, or Action and Func<T> delegates WPF’s ICommand with Execute, CanExecute and CanExecuteChanged; libraries such as MediatR send request objects to handlers
                Go A small interface, or a func() value; serialized jobs as a type name plus a []byte payload Job libraries such as asynq create tasks from a type name and a payload
                TypeScript An object with execute and undo, or a closure; serialized commands as plain objects with a type VS Code’s commands.registerCommand and executeCommand; Redux actions are plain objects with a type

                In languages with first-class functions, a command with only execute is a closure, and that’s usually the right choice. A class earns its place when the command also needs undo, a label, canExecute, merging, or a serializable form.

                Pattern Looks like Command because The difference
                Strategy Both put behavior in an object behind an interface A strategy is how to do a step the caller is already doing, chosen now and called for an answer. A command is what to do, a request that can be stored, undone or run later by someone else.
                Memento Both are used to build undo A memento is a snapshot of state, with no behavior. A command is an action that knows how to reverse itself. Undo can use either, or both.
                Observer and events Both are messages that decouple a sender from a receiver A command asks for something to happen (SendInvoice) and has one handler, which may refuse. An event says something already happened (InvoiceSent) and has any number of listeners. Redux’s style guide recommends naming actions as events for this reason.
                A plain callback or lambda A callback is code passed to run later A command with only execute and no data to keep is a callback. Use a function until you need undo, a label, canExecute, or a serializable form.
                Chain of responsibility Both pass a request object to a handler In a chain, several handlers are tried in turn until one takes the request. A command usually goes to one known handler. They combine: a request object travelling down a chain is a command.
                Event sourcing Both keep a log of things to replay Event sourcing stores the events that happened, and rebuilds state from them. Commands are the requests that produced those events; a command can be rejected, an event can’t.

                On this site:

                • Tic-tac-toe: each move is a small value that can be undone, and the page explains when a full Command class would be worth it.
                • Text editor with undo: the Medium level, worked as a full design problem.
                • Strategy, for the pattern most often confused with this one.

                In real systems, you’ll find commands in:

                • Menu and toolbar actions in desktop frameworks, and command palettes in editors such as VS Code.
                • Undo in every document editor.
                • Transaction logs, and database migrations. Each migration is a versioned step, applied in order.
                • Background job systems (Celery, Sidekiq, asynq).
                • The command side of CQRS systems.
                Interviewer asksHow would you implement undo and redo?

                Make every action a command with execute and undo, where the command captures what it needs to reverse itself when it runs. Keep an undo stack and a redo stack: run pushes and clears redo, undo moves a command from undo to redo, redo moves it back. Merge consecutive typing into one step and bound the stacks. The alternative is snapshots (Memento), which are simpler but store the whole state per step.

                Interviewer asksWhat's the difference between Command and Strategy?

                A strategy answers “how should this step be done?” for code that is already running. It’s called for its result. A command is “do this”, captured as an object. So it can be stored, undone, logged, or run later by someone else. Strategies are usually swapped by configuration. Commands are usually created per request.

                Interviewer asksHow do you make a queued command safe to retry?

                Assume at-least-once delivery. Give every command an idempotency key, derived from what makes it unique (the order id for an invoice). Record finished keys durably, and skip duplicates. Pass the key to outside side effects that support one. Retry temporary errors with capped exponential backoff and jitter. Send permanent errors and repeated failures to a dead-letter queue. And version the payload, so old jobs still parse after a deploy.

                Interviewer asksWhat about actions that can't be undone, like sending an email?

                There are three options. Keep them off the undo stack, and say so in the UI. Delay them: Gmail’s “undo send” holds the email for a few seconds before sending. Or offer a compensating command. It does a new action, instead of reversing the old one: a refund for a charge, a correction for an email. Undo inside a document is a true inverse. Undo in the outside world is usually compensation.

                Interviewer asksWhen is a command just a function?

                When it runs once, right away, and has no undo, no label, no canExecute and no need to be stored or sent anywhere. In Java that’s a Runnable, in C# an Action, in Python, Go and TypeScript a function value. Promote it to a class when one of those needs appears.

                Decision We chose We gave up
                Where an action’s logic lives One command object, shared by every trigger A class per action and one more hop
                Enabling buttons canExecute on the command, asked by every invoker Nothing; it removes drift
                Undo Commands with undo, data captured on execute Every inverse must be exactly right
                History Undo and redo stacks, redo cleared on a new edit, bounded Old steps and the undone branch are lost
                Typing Consecutive keys merged into one step A rule to tune (time, word boundaries)
                Commands across processes Type, version, key and payload, handled by type Payloads are a contract between releases
                Delivery At least once, with idempotency keys from the business Every handler and side effect must be idempotent
                Failures Capped backoff for transient errors, a dead-letter queue for the rest Someone must watch and replay the DLQ
                • Strategy, for behavior chosen now rather than requested for later
                • Observer, for events: what happened, rather than what to do
                • Message queues, for leases, acks and dead-letter queues in depth
                • Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, Design Patterns, the Command chapter