Command
The Command pattern wraps a request as an object — with execute, sometimes undo, queueing, logging, and replay. Useful for menus, undo/redo, transactional workflows, distributed task queues, and macro recording — anywhere “do this thing later or differently” is the requirement.
Encapsulate, undo, queue, macro
EXAMPLE
// 1) The Command interface
interface Command {
execute(): void;
undo?(): void;
description(): string;
}
// 2) Concrete commands — text editor example
class InsertText implements Command {
constructor(private doc: Document, private pos: number, private text: string) {}
execute() { this.doc.insert(this.pos, this.text); }
undo() { this.doc.delete(this.pos, this.text.length); }
description() { return `Insert \"${this.text}\" at ${this.pos}`; }
}
class DeleteText implements Command {
private removed = '';
constructor(private doc: Document, private pos: number, private len: number) {}
execute() { this.removed = this.doc.read(this.pos, this.len); this.doc.delete(this.pos, this.len); }
undo() { this.doc.insert(this.pos, this.removed); }
description() { return `Delete ${this.len} chars at ${this.pos}`; }
}
class FormatBold implements Command {
constructor(private doc: Document, private from: number, private to: number) {}
execute() { this.doc.format(this.from, this.to, 'bold', true); }
undo() { this.doc.format(this.from, this.to, 'bold', false); }
description() { return `Bold range ${this.from}..${this.to}`; }
}
// 3) Invoker — a generic 'do' that supports undo/redo history
class CommandHistory {
private undoStack: Command[] = [];
private redoStack: Command[] = [];
execute(cmd: Command) {
cmd.execute();
this.undoStack.push(cmd);
this.redoStack = []; // new action invalidates redo
}
undo() {
const cmd = this.undoStack.pop();
if (!cmd) return;
cmd.undo?.();
this.redoStack.push(cmd);
}
redo() {
const cmd = this.redoStack.pop();
if (!cmd) return;
cmd.execute();
this.undoStack.push(cmd);
}
canUndo() { return this.undoStack.length > 0; }
canRedo() { return this.redoStack.length > 0; }
}
// Usage
const doc = new Document();
const history = new CommandHistory();
history.execute(new InsertText(doc, 0, 'Hello'));
history.execute(new InsertText(doc, 5, ', world'));
history.undo(); // 'Hello'
history.redo(); // 'Hello, world'
// 4) Menu / button binding — UI knows nothing about behaviour
class Button {
constructor(private label: string, private cmd: Command) {}
click() { this.cmd.execute(); }
}
const saveBtn = new Button('Save', new SaveCommand(doc));
const boldBtn = new Button('Bold', new FormatBold(doc, selStart, selEnd));
// The Button doesn't know how to save or bold — just executes a command.
// 5) Macro command — composite of commands
class MacroCommand implements Command {
constructor(private commands: Command[]) {}
execute() { for (const c of this.commands) c.execute(); }
undo() { for (const c of [...this.commands].reverse()) c.undo?.(); }
description() { return `Macro: ${this.commands.length} steps`; }
}
const recordedMacro = new MacroCommand([
new InsertText(doc, 0, '# Heading\\n'),
new InsertText(doc, 10, '\\nIntro paragraph.\\n'),
new FormatBold(doc, 0, 9),
]);
history.execute(recordedMacro);
history.undo(); // undoes the whole macro
// 6) Distributed task queues — Bull, Sidekiq, Celery, Resque
// 'Commands' here are jobs: serialized requests processed by workers
type EmailJob = { type: 'email'; to: string; subject: string; html: string };
type ReportJob = { type: 'report'; userId: string; date: string };
type Job = EmailJob | ReportJob;
async function enqueue(job: Job) {
await redis.lpush('jobs', JSON.stringify(job));
}
// Worker side
async function workerLoop() {
while (true) {
const raw = await redis.brpop('jobs', 0);
const job: Job = JSON.parse(raw[1]);
switch (job.type) {
case 'email': await sendEmail(job); break;
case 'report': await buildReport(job); break;
}
}
}
// Benefits:
// • Decouple producer + consumer
// • Retry, schedule, prioritise
// • Replay from a log for debugging
// • Multiple workers for parallelism
// 7) HTTP API as commands
// 'POST /commands' with { type, payload } → server validates + executes
// Audit log of every command for compliance + replay
// 8) Database transactional outbox
// Application writes commands to an OUTBOX table in the same DB transaction as state change.
// A relay process picks them up and dispatches reliably.
// Useful when 'change state + send event' must be ATOMIC (no dual writes).
// 9) Undo/Redo for SaaS apps
// • Maintain a per-user/per-document command history
// • Persist to localStorage or backend for cross-session redo
// • Compact history at session boundaries to bound memory
// • Snapshot every N commands for fast rewind without replaying all undos
// 10) Event sourcing — extension of command
// Don't store current state — store the SEQUENCE OF EVENTS that built it.
// Rebuild state from events; replay for time-travel debugging; perfect audit.
// Frameworks: EventStoreDB, Marten, Axon, custom DIY.
// 11) When NOT to use Command
// • Simple synchronous calls — direct method call is clearer
// • Pure function transforms — no state to encapsulate
// • One-shot operations with no queueing, logging, or undo needs
// 12) Variants + related patterns
// • Memento — saves state for undo without changing the receiver's API
// • Strategy — selects an algorithm; Command represents a CALL
// • Mediator — central routing of requests
// • Chain of Responsibility — request passed along handlers
// • Observer — notify after a Command runs
// 13) Real-world examples
// • IDEs: every action (typing, formatting, refactor) is a Command
// • Photoshop: history panel is a command stack
// • Browsers: history.pushState ≈ navigation command
// • Game engines: Input → Command → System (good for replays + bots)
// • RPC frameworks: gRPC method calls are commands
// • Saga orchestration in microservices: each step + compensation is a command
// 14) Common bugs
// • Forgot to clear redo stack on new action → re-do a command from before, confusing UX
// • Macro undo in wrong order → must REVERSE the list
// • Mutating the receiver inside a command's constructor instead of execute → undo / replay impossible
// • Side-effect commands without idempotency → retries cause duplicate emails / charges
// • Serialising commands with closures over local state → can't replay across processes
// • Massive macro objects retaining references → memory bloat in long sessions
// • Treating commands as void functions → lose the ability to log/audit/retry
// • Missing undo on a destructive command → 'oops, can't recover'
Why it matters
The Command pattern turns “do this” into an object you can stash, log, undo, queue, replay, or distribute. Reach for it whenever you need undo/redo, macro recording, audit trails, durable task queues, or event sourcing — and make sure each command is serialisable + idempotent if it’s going to be retried.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Encapsulate an action as an object — supports queueing, undo, replay.
class MoveCommand {
constructor(target, dx, dy) { Object.assign(this, { target, dx, dy }); }
do() { this.target.x += this.dx; this.target.y += this.dy; }
undo() { this.target.x -= this.dx; this.target.y -= this.dy; }
}
Try it Yourself »
Exercise
Methods a Command typically exposes.
do() /
()
Four letters.
Discussion
Loading…