What is Command design pattern?
Command design pattern turns an action into an object you can store, pass around, queue, or reverse.
Theory
TL;DR
- Restaurant analogy: the waiter hands an order slip (command object) to the kitchen (receiver). The kitchen executes without knowing the customer.
- Core idea: wrap a request in an object so the caller and the executor know nothing about each other.
- Both
execute()andundo()live on the command object, making any action reversible. - Use it when you need undo/redo, task queues, or macro commands.
- For simple one-off calls with no queue or undo, skip it and call the method directly.
Quick example
class Light {
on() { console.log('Light on'); }
off() { console.log('Light off'); }
}
class LightOnCommand {
constructor(light) { this.light = light; }
execute() { this.light.on(); } // Light on
}
class RemoteControl {
setCommand(cmd) { this.slot = cmd; }
pressButton() { this.slot.execute(); }
}
const remote = new RemoteControl();
remote.setCommand(new LightOnCommand(new Light()));
remote.pressButton(); // Light onRemoteControl knows nothing about Light. It only calls execute(). Replace LightOnCommand with a DimmerOnCommand later and the remote stays unchanged.
Key difference from direct method calls
When you call light.on() directly from the remote, the remote and the light are coupled. Add a dimmer and you rewrite the remote. With Command, the remote holds a command object and calls execute(). The sender never changes when the receiver does.
When to use
- Undo/redo needed: Command stores enough state in the constructor to reverse itself.
- Task queues: actions become plain objects you can serialize and defer.
- Macro commands: chain multiple commands into one composite action.
- Logging and transactions: wrap execute with pre/post hooks.
- Simple one-off call with no queue or undo: skip Command. The object allocation adds nothing here.
How it works in JavaScript
Commands are plain objects in V8. The execute() call dispatches via prototypal inheritance, no special runtime magic needed. State capture happens through closures and object references stored in the constructor. Node.js event loop queues commands naturally with setImmediate; browser UI commands often use requestAnimationFrame.
Common mistakes
1. Forgetting to store state in the constructor
// Wrong: this.text is undefined in undo
class AddTextCommand {
execute() { editor.add('hello'); }
undo() { editor.remove(this.text); } // TypeError: undefined
}
// Right
class AddTextCommand {
constructor(editor, text) {
this.editor = editor;
this.text = text; // stored for undo
}
execute() { this.editor.add(this.text); }
undo() { this.editor.remove(this.text); }
}2. Mutating shared state without a snapshot
// Wrong: undo pops but re-execute grows the array again
constructor(editor) { this.editor = editor; } // no backup
// Right: snapshot on construction
constructor(editor, text) {
this.backup = [...editor.text]; // saved before execute
}
undo() { this.editor.text = this.backup; } // full restoreThis is the most common undo-queue bug you will see in production. The array mutates across re-executes and the undo logic silently drifts.
3. Missing null-check on history.pop()
// Crashes on empty history
undo() { const cmd = history.pop(); cmd.undo(); }
// Safe
undo() { const cmd = history.pop(); cmd?.undo(); }4. Overusing Command for simple actions
One function, no queue, no undo? Call it directly. Each command object costs an allocation and a GC cycle. In tight loops that adds up.
Real-world usage
- Redux: every dispatched action is a command object; reducers are the receivers.
- BullMQ (Node.js): queued jobs are command objects with built-in retry logic.
- Figma and Photoshop: the entire undo history is a command stack.
- React use-undo library: wraps state changes as reversible commands.
- jQuery UI: uses commands internally for sortable drag-and-drop undo.
Follow-up questions
Q: How would you add undo to the canvas example?
A: Store a snapshot of state in the command constructor. In undo(), restore from that snapshot instead of trying to guess the reverse operation.
Q: How does Command enable macro commands?
A: A MacroCommand holds an array of commands. execute() loops forward; undo() loops in reverse. If one command throws, roll back all already-executed ones before rethrowing.
Q: What is the difference between Command and Strategy?
A: Strategy swaps an algorithm at runtime but executes it immediately. Command wraps a request so it can be deferred, queued, or undone. Strategy is about how; Command is about when and whether.
Q: How do you handle async commands in Node.js?
A: Return a Promise from execute() and await it in the invoker. The pattern stays the same; you just need to handle partial rollback if a command in a macro chain rejects.
Q: Design an undoable shopping cart with discounts applied.
A: Each action (add item, apply discount) becomes a command. Snapshot totals before each execute. Rollback restores the previous snapshot. For concurrent updates, add a version counter to each command and reject stale ones on conflict.
Examples
Basic: remote control
class Light {
on() { console.log('Light on'); }
off() { console.log('Light off'); }
}
class LightOnCommand {
constructor(light) { this.light = light; }
execute() { this.light.on(); }
undo() { this.light.off(); }
}
class LightOffCommand {
constructor(light) { this.light = light; }
execute() { this.light.off(); }
undo() { this.light.on(); }
}
class RemoteControl {
constructor() { this.history = []; }
press(cmd) { cmd.execute(); this.history.push(cmd); }
undo() { this.history.pop()?.undo(); }
}
const remote = new RemoteControl();
const light = new Light();
remote.press(new LightOnCommand(light)); // Light on
remote.press(new LightOffCommand(light)); // Light off
remote.undo(); // Light on (reversed)The remote holds command objects, not a direct reference to the light. Add any new command and the remote code stays the same.
Intermediate: canvas with undo history
class Canvas {
constructor() { this.elements = []; }
add(shape) { this.elements.push(shape); console.log(`Added ${shape}`); }
remove(shape) {
this.elements = this.elements.filter(s => s !== shape);
console.log(`Removed ${shape}`);
}
}
class AddShapeCommand {
constructor(canvas, shape) {
this.canvas = canvas;
this.shape = shape;
}
execute() { this.canvas.add(this.shape); }
undo() { this.canvas.remove(this.shape); }
}
class History {
constructor() { this.stack = []; }
execute(cmd) { cmd.execute(); this.stack.push(cmd); }
undo() { this.stack.pop()?.undo(); }
}
const canvas = new Canvas();
const history = new History();
history.execute(new AddShapeCommand(canvas, 'circle')); // Added circle
history.execute(new AddShapeCommand(canvas, 'square')); // Added square
history.undo(); // Removed square
console.log(canvas.elements); // ['circle']This is roughly how Figma tracks drawing history. Each user action becomes one command on the stack.
Advanced: transactional macro command
class MacroCommand {
constructor(commands) { this.commands = commands; }
execute() {
const done = [];
for (const cmd of this.commands) {
try {
cmd.execute();
done.push(cmd);
} catch (e) {
done.slice().reverse().forEach(c => c.undo()); // rollback
throw e;
}
}
}
undo() {
this.commands.slice().reverse().forEach(cmd => cmd.undo());
}
}
class ApiCallCommand {
constructor(id) { this.id = id; }
execute() {
if (Math.random() < 0.3) throw new Error('Network timeout');
console.log(`Processed ${this.id}`);
}
undo() { console.log(`Rolled back ${this.id}`); }
}
const macro = new MacroCommand([
new ApiCallCommand(1),
new ApiCallCommand(2),
new ApiCallCommand(3),
]);
try {
macro.execute();
} catch (e) {
console.log('Macro failed, all changes rolled back');
}Reverse-order undo matters. If step 3 fails after steps 1 and 2 succeeded, you undo 2 first, then 1. That is exactly how database transactions handle partial failure too.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.