InterviewKata
Design patterns · 16 of 26 · Choosing behaviour

Command

Command turns a request into an object that can run itself and often reverse itself, so requests can be queued, logged, scheduled and undone. This page rebuilds Pedal's operator console on commands, with undo, an audit log and a night-shift queue.

1. The pain

Pedal's operators run the fleet from a console: disable a broken dock, move bikes from a full station to an empty one, change a station's price for an event. Each button called the fleet directly: fleet.move("S-12", "S-31", 5). The call happened, and then it was gone.

An operator moved five bikes to the wrong station; with no undo, a crew drove across town to move them back. The night shift wanted to queue moves to run at 3 a.m., and auditors wanted a record of who did what. Neither was possible, because a request existed only for the instant it ran.

2. The idea in one picture

In a restaurant, the waiter doesn't shout your order into the kitchen. They write it on a ticket. The ticket can wait on the rail until a cook is free, be read back, be crossed out if you change your mind, and stay on the spike at the end of the night as a record. The request has become a thing.

Command turns a request into an object that knows how to carry itself out (run()), and often how to reverse itself (undo()). The object that triggers it (the invoker, here OperatorConsole) doesn't need to know what any command does; it just runs it, keeps it in a history, and writes it to a log. Commands can be queued, scheduled, retried, sent over a network or replayed.

Undo needs care. A command must remember enough to reverse itself, and it should be undone only if it actually ran: the refused move in section 5 never enters the history.

3. The roles

RoleIts jobIn Pedal
CommandThe interface: run, undo, describe.Command
Concrete commandOne request, with the data it needs and the receiver it acts on.DisableDock("D9"), MoveBikes("S-12", "S-31", 5)
ReceiverThe object that does the real work.Fleet
InvokerRuns commands; keeps history and a log; knows no command types.OperatorConsole
ClientCreates commands and hands them to the invoker.The console's buttons

4. Before and after

The console both ways (Python sketch; the full program is in section 5).

Without the pattern

def on_move_clicked(src, dst, n):
    fleet.move(src, dst, n)   # and then it's gone

def on_disable_clicked(dock):
    fleet.disable(dock)

# undo?      nothing to undo
# run at 3am? nothing to queue
# audit?     nothing to log

With it

def on_move_clicked(src, dst, n):
    console.submit(MoveBikes(src, dst, n))

class MoveBikes(Command):
    def run(self, fleet):
        fleet.bikes[self.src] -= self.count
        fleet.bikes[self.dst] += self.count
    def undo(self, fleet):
        fleet.bikes[self.dst] -= self.count
        fleet.bikes[self.src] += self.count
The next changeWithout the patternWith it
Undo the last actionWrite the reverse by hand, each timeconsole.undo()
Queue moves for 3 a.m.A separate job system with its own request formatStore the command objects; run them later
Audit who did whatLogging added to every buttonThe invoker logs every command it runs

Once a request is an object, everything that can be done with objects (store, queue, log, reverse) can be done with requests.

5. The code

Two command classes act on a Fleet; the OperatorConsole runs them, keeps successful ones for undo, and logs everything, including the refused move.

from __future__ import annotations
from abc import ABC, abstractmethod


class Fleet:
    """The receiver: the real objects that commands act on."""

    def __init__(self) -> None:
        self.bikes = {"S-12": 7, "S-31": 3}
        self.disabled: set[str] = set()

    def state(self) -> str:
        docks = ", ".join(sorted(self.disabled)) or "none"
        return f"S-12 {self.bikes['S-12']}, S-31 {self.bikes['S-31']} | disabled docks: {docks}"


# ---- Each request is an object: it can be run, undone, stored and described. ----
class Command(ABC):
    @abstractmethod
    def run(self, fleet: Fleet) -> None: ...

    @abstractmethod
    def undo(self, fleet: Fleet) -> None: ...

    @abstractmethod
    def describe(self) -> str: ...


class DisableDock(Command):
    def __init__(self, dock: str) -> None:
        self.dock = dock

    def run(self, fleet: Fleet) -> None: fleet.disabled.add(self.dock)
    def undo(self, fleet: Fleet) -> None: fleet.disabled.discard(self.dock)
    def describe(self) -> str: return f"disable {self.dock}"


class MoveBikes(Command):
    def __init__(self, src: str, dst: str, count: int) -> None:
        self.src, self.dst, self.count = src, dst, count

    def run(self, fleet: Fleet) -> None:
        if fleet.bikes[self.src] < self.count:
            raise ValueError(f"{self.src} has only {fleet.bikes[self.src]}")
        fleet.bikes[self.src] -= self.count
        fleet.bikes[self.dst] += self.count

    def undo(self, fleet: Fleet) -> None:
        fleet.bikes[self.dst] -= self.count
        fleet.bikes[self.src] += self.count

    def describe(self) -> str: return f"move {self.count} bikes {self.src} -> {self.dst}"


# ---- The invoker: runs commands, keeps a history for undo and a log for audit. It knows no command types. ----
class OperatorConsole:
    def __init__(self, fleet: Fleet) -> None:
        self.fleet = fleet
        self.history: list[Command] = []
        self.log: list[str] = []

    def submit(self, cmd: Command) -> str:
        try:
            cmd.run(self.fleet)
        except ValueError as e:
            self.log.append(f"refused {cmd.describe()}")
            return f"run: {cmd.describe()} -> refused: {e}"
        self.history.append(cmd)
        self.log.append(cmd.describe())
        return f"run: {cmd.describe()} -> {self.fleet.state()}"

    def undo(self) -> str:
        cmd = self.history.pop()
        cmd.undo(self.fleet)
        self.log.append(f"undo {cmd.describe()}")
        return f"undo: {cmd.describe()} -> {self.fleet.state()}"


if __name__ == "__main__":
    console = OperatorConsole(Fleet())
    print(console.submit(DisableDock("D9")))
    print(console.submit(MoveBikes("S-12", "S-31", 5)))
    print(console.submit(MoveBikes("S-12", "S-31", 4)))
    print(console.undo())
    print(console.undo())
    print("audit log: " + "; ".join(console.log))
// The receiver: the real objects that commands act on.
class Fleet {
  readonly bikes: Record<string, number> = { "S-12": 7, "S-31": 3 };
  readonly disabled = new Set<string>();
  state(): string {
    const docks = [...this.disabled].sort().join(", ") || "none";
    return `S-12 ${this.bikes["S-12"]}, S-31 ${this.bikes["S-31"]} | disabled docks: ${docks}`;
  }
}

class Refused extends Error {}

// ---- Each request is an object: it can be run, undone, stored and described. ----
interface Command {
  run(fleet: Fleet): void;
  undo(fleet: Fleet): void;
  describe(): string;
}

class DisableDock implements Command {
  constructor(private readonly dock: string) {}
  run(fleet: Fleet) { fleet.disabled.add(this.dock); }
  undo(fleet: Fleet) { fleet.disabled.delete(this.dock); }
  describe() { return `disable ${this.dock}`; }
}

class MoveBikes implements Command {
  constructor(private readonly src: string, private readonly dst: string, private readonly count: number) {}
  run(fleet: Fleet) {
    if (fleet.bikes[this.src] < this.count) throw new Refused(`${this.src} has only ${fleet.bikes[this.src]}`);
    fleet.bikes[this.src] -= this.count;
    fleet.bikes[this.dst] += this.count;
  }
  undo(fleet: Fleet) { fleet.bikes[this.dst] -= this.count; fleet.bikes[this.src] += this.count; }
  describe() { return `move ${this.count} bikes ${this.src} -> ${this.dst}`; }
}

// ---- The invoker: runs commands, keeps a history for undo and a log for audit. It knows no command types. ----
class OperatorConsole {
  private readonly history: Command[] = [];
  readonly log: string[] = [];
  constructor(private readonly fleet: Fleet) {}

  submit(cmd: Command): string {
    try {
      cmd.run(this.fleet);
    } catch (e) {
      if (!(e instanceof Refused)) throw e;
      this.log.push(`refused ${cmd.describe()}`);
      return `run: ${cmd.describe()} -> refused: ${e.message}`;
    }
    this.history.push(cmd);
    this.log.push(cmd.describe());
    return `run: ${cmd.describe()} -> ${this.fleet.state()}`;
  }

  undo(): string {
    const cmd = this.history.pop()!;
    cmd.undo(this.fleet);
    this.log.push(`undo ${cmd.describe()}`);
    return `undo: ${cmd.describe()} -> ${this.fleet.state()}`;
  }
}

const consoleUi = new OperatorConsole(new Fleet());
console.log(consoleUi.submit(new DisableDock("D9")));
console.log(consoleUi.submit(new MoveBikes("S-12", "S-31", 5)));
console.log(consoleUi.submit(new MoveBikes("S-12", "S-31", 4)));
console.log(consoleUi.undo());
console.log(consoleUi.undo());
console.log("audit log: " + consoleUi.log.join("; "));
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;

public class Main {
    // The receiver: the real objects that commands act on.
    static class Fleet {
        final Map<String, Integer> bikes = new HashMap<>(Map.of("S-12", 7, "S-31", 3));
        final TreeSet<String> disabled = new TreeSet<>();
        String state() {
            String docks = disabled.isEmpty() ? "none" : String.join(", ", disabled);
            return "S-12 " + bikes.get("S-12") + ", S-31 " + bikes.get("S-31") + " | disabled docks: " + docks;
        }
    }

    static class Refused extends Exception {
        private static final long serialVersionUID = 1L;
        Refused(String message) { super(message); }
    }

    // ---- Each request is an object: it can be run, undone, stored and described. ----
    interface Command {
        void run(Fleet fleet) throws Refused;
        void undo(Fleet fleet);
        String describe();
    }

    static class DisableDock implements Command {
        private final String dock;
        DisableDock(String dock) { this.dock = dock; }
        public void run(Fleet fleet) { fleet.disabled.add(dock); }
        public void undo(Fleet fleet) { fleet.disabled.remove(dock); }
        public String describe() { return "disable " + dock; }
    }

    static class MoveBikes implements Command {
        private final String src, dst;
        private final int count;
        MoveBikes(String src, String dst, int count) { this.src = src; this.dst = dst; this.count = count; }
        public void run(Fleet fleet) throws Refused {
            if (fleet.bikes.get(src) < count) throw new Refused(src + " has only " + fleet.bikes.get(src));
            fleet.bikes.merge(src, -count, Integer::sum);
            fleet.bikes.merge(dst, count, Integer::sum);
        }
        public void undo(Fleet fleet) { fleet.bikes.merge(dst, -count, Integer::sum); fleet.bikes.merge(src, count, Integer::sum); }
        public String describe() { return "move " + count + " bikes " + src + " -> " + dst; }
    }

    // ---- The invoker: runs commands, keeps a history for undo and a log for audit. It knows no command types. ----
    static class OperatorConsole {
        private final Fleet fleet;
        private final Deque<Command> history = new ArrayDeque<>();
        final List<String> log = new ArrayList<>();
        OperatorConsole(Fleet fleet) { this.fleet = fleet; }

        String submit(Command cmd) {
            try {
                cmd.run(fleet);
            } catch (Refused e) {
                log.add("refused " + cmd.describe());
                return "run: " + cmd.describe() + " -> refused: " + e.getMessage();
            }
            history.push(cmd);
            log.add(cmd.describe());
            return "run: " + cmd.describe() + " -> " + fleet.state();
        }

        String undo() {
            Command cmd = history.pop();
            cmd.undo(fleet);
            log.add("undo " + cmd.describe());
            return "undo: " + cmd.describe() + " -> " + fleet.state();
        }
    }

    public static void main(String[] args) {
        OperatorConsole console = new OperatorConsole(new Fleet());
        System.out.println(console.submit(new DisableDock("D9")));
        System.out.println(console.submit(new MoveBikes("S-12", "S-31", 5)));
        System.out.println(console.submit(new MoveBikes("S-12", "S-31", 4)));
        System.out.println(console.undo());
        System.out.println(console.undo());
        System.out.println("audit log: " + String.join("; ", console.log));
    }
}

Output (the same in all three languages)

run: disable D9 -> S-12 7, S-31 3 | disabled docks: D9
run: move 5 bikes S-12 -> S-31 -> S-12 2, S-31 8 | disabled docks: D9
run: move 4 bikes S-12 -> S-31 -> refused: S-12 has only 2
undo: move 5 bikes S-12 -> S-31 -> S-12 7, S-31 3 | disabled docks: D9
undo: disable D9 -> S-12 7, S-31 3 | disabled docks: none
audit log: disable D9; move 5 bikes S-12 -> S-31; refused move 4 bikes S-12 -> S-31; undo move 5 bikes S-12 -> S-31; undo disable D9

6. Try it

Run the operator console

Issue commands, undo and redo them, or queue them for the night shift.

History (undo stack)

    Queued for tonight

      Audit log

        7. When not to use it

        What you get

        • Undo and redo: each command knows how to reverse itself.
        • Queues and schedules: run later, on another machine, or retry.
        • Audit for free: the invoker logs every command.
        • Decoupled triggers: buttons, scripts and APIs submit the same commands.

        What you pay

        • A class per action: many small classes.
        • Undo is real work: commands must store what they need to reverse, and order matters.
        • Stale commands: a queued move may no longer make sense at 3 a.m.
        • Overkill for fire-and-forget calls: if nothing is queued, logged or undone, call the method.
        If…Use instead
        You only need to restore an earlier snapshot22 Memento (often combined with command)
        Requests pass along handlers until one accepts17 Chain of responsibility
        Commands must survive crashes and run across servicesA durable job queue or message broker
        The action is a simple callback with nothing to undoPass a function

        8. How it goes wrong

        Undo that doesn't match runIf run() moved only the bikes that were free but undo() moves the full count back, numbers drift. Record what actually happened.
        Recording refused commands in historyUndoing a command that never ran corrupts state. Push to history only after success.
        Commands that hold live objects for laterA queued command holding a stale fleet snapshot acts on old data. Hold ids; look things up when it runs.
        Smart invokersAn invoker with if isinstance(cmd, MoveBikes) has lost the point. It should only run, store and log.

        9. Remember it and say it

        Memory hook: "Write it on a ticket"A request becomes an object: it can wait, be read back, crossed out, or kept as a record.
        How this hook works: what “Write it on a ticket” means

        What the slogan means, phrase by phrase

        “Write it”
        Create a command object.
        “on a ticket”
        The request has data and can run itself.
        “crossed out”
        undo().

        Why this picture fits: Restaurant tickets turn shouted requests into things that can queue, be cancelled and be audited.

        The story → the concept

        The restaurantThe code
        The ticketMoveBikes, DisableDock
        The waiterThe console button (client)
        The rail and the spikeHistory and audit log in OperatorConsole
        The kitchenFleet (the receiver)

        Watch it on an example

        1. The operator moves 5 bikes from S-12 to S-31: a MoveBikes command runs and goes on the history.
        2. A second move of 4 is refused (S-12 has only 2) and never enters the history.
        3. Undo pops the first move and reverses it: S-12 is back to 7.

        The trap it saves you from

        Calling the receiver directly, so requests vanish the moment they run.

        It still works when…

        Yes: editor undo stacks, job queues, database migrations (up and down) and game input replay.

        In an interview or code review

        10. Check yourself

        What does Command turn into an object?

        Then it can be stored, queued, logged and undone.

        Which object knows how to reverse a bike move?

        Each command knows its own undo.

        A move is refused. Should it go on the undo history?

        Otherwise undo corrupts state.

        What should the invoker know about each command?

        Run, undo, describe.

        Commands queued for 3 a.m. should hold…

        State may have changed by then.

        11. Go deeper

        ResourceWhy
        22 MementoUndo by snapshot rather than by reversal.
        Architecture styles 10: Event-drivenRequests and events as messages between services.
        Python queueQueues of work items: commands waiting for a worker.
        Wikipedia: Command patternUses, including undo and transactions.