iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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() / ()

Discussion

Loading…