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
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
| Role | Its job | In Pedal |
|---|---|---|
| Command | The interface: run, undo, describe. | Command |
| Concrete command | One request, with the data it needs and the receiver it acts on. | DisableDock("D9"), MoveBikes("S-12", "S-31", 5) |
| Receiver | The object that does the real work. | Fleet |
| Invoker | Runs commands; keeps history and a log; knows no command types. | OperatorConsole |
| Client | Creates 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 logWith 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 change | Without the pattern | With it |
|---|---|---|
| Undo the last action | Write the reverse by hand, each time | console.undo() |
| Queue moves for 3 a.m. | A separate job system with its own request format | Store the command objects; run them later |
| Audit who did what | Logging added to every button | The 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.
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 snapshot | 22 Memento (often combined with command) |
| Requests pass along handlers until one accepts | 17 Chain of responsibility |
| Commands must survive crashes and run across services | A durable job queue or message broker |
| The action is a simple callback with nothing to undo | Pass a function |
8. How it goes wrong
run() moved only the bikes that were free but undo() moves the full count back, numbers drift. Record what actually happened.if isinstance(cmd, MoveBikes) has lost the point. It should only run, store and log.9. Remember it and say it
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 restaurant | The code |
|---|---|
| The ticket | MoveBikes, DisableDock |
| The waiter | The console button (client) |
| The rail and the spike | History and audit log in OperatorConsole |
| The kitchen | Fleet (the receiver) |
Watch it on an example
- The operator moves 5 bikes from S-12 to S-31: a
MoveBikescommand runs and goes on the history. - A second move of 4 is refused (S-12 has only 2) and never enters the history.
- 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
- Say what the object buys: "Operator actions are commands, so the console can undo them, queue them for the night and log them for audit."
- Name the roles: commands carry the data;
Fleetis the receiver; the console is the invoker and knows no command types. - Mention undo's rule: only successful commands enter the history; each stores what it needs to reverse.
- Scale it up: a queue of commands across services is a job queue or message broker; event sourcing stores commands' results as events.
10. Check yourself
What does Command turn into an object?
Which object knows how to reverse a bike move?
A move is refused. Should it go on the undo history?
What should the invoker know about each command?
Commands queued for 3 a.m. should hold…
11. Go deeper
| Resource | Why |
|---|---|
| 22 Memento | Undo by snapshot rather than by reversal. |
| Architecture styles 10: Event-driven | Requests and events as messages between services. |
Python queue | Queues of work items: commands waiting for a worker. |
| Wikipedia: Command pattern | Uses, including undo and transactions. |