Skip to content

The Command Pattern in JavaScript — A detailed Guide

This guide provides a deep, practical, and production-focused exploration of the Command design pattern, tailored for developers as we build resilient, testable, and extensible systems in JavaScript. The Command pattern encapsulates a request as an object, decoupling the sender of a request from the receiver thus providing flexibility for undo/redo, queuing, logging, retrying, batching (macros), and remote execution. In JavaScript, commands are particularly useful for UI actions, task queues, collaborative editors, and distributed systems.

This guide covers:

  • Core concept, motivation, and anatomy
  • Minimal and idiomatic JavaScript implementations (vanilla + ES6 classes + functional)
  • Advanced features: undo/redo, macros, logging, transactions, async/await, retries, timeouts
  • Integration with other patterns: Mediator, Strategy, Chain of Responsibility, Event Sourcing
  • Testing, performance, and operational concerns
  • Real-world use cases and anti-patterns
  • Interview questions and a code review checklist

2. Components of the command design Pattern

The command design pattern is made up of:

  • Command — Interface/shape for executing an action (and optionally undoing it)."the blueprint/instruction that says “worker, please build this wall now."
  • ConcreteCommand — Implements the Command interface; holds a reference to the receiver and parameters.
  • Receiver — The object that performs the actual work. "the worker who physically builds the house."
  • Invoker — Requests the command to perform the action (e.g., UI controller, scheduler).
  • Client — Constructs ConcreteCommand instances and wires together the invoker and receiver.

Minimal shape in JS:

js
// Command shape definition
// An object that has at least an execute() method, and optionally an undo() method.
 { execute(): Promise|any, undo?: () => any }

3. Simple JavaScript examples of command design pattern

3.1 Functional Command

Here, commands are represented as simple objects with execute and undo functions.

js
// Command factory: produces Command objects
const makeCommand = (executeFn, undoFn) => ({
  execute: (...args) => executeFn(...args),
  undo: undoFn ? (...args) => undoFn(...args) : undefined,
});

// Receiver: the object that actually performs the work
const receiver = { value: 0 };

// ConcreteCommand: a specific command instance tied to a receiver
const inc = makeCommand(
  () => { receiver.value += 1; }, // execute
  () => { receiver.value -= 1; }  // undo
);

// Invoker: the code that triggers commands (here we call inc.execute / inc.undo)
inc.execute();
console.log(receiver.value); // 1

inc.undo();
console.log(receiver.value); // 0

// Client: the setup code that wires everything together -creates the receiver, builds 
// commands, and assigns them)

3.2 Class-Based ES6 Implementation

This style is similar to formal object oriented programming implementation often used in enterprise systems. A base Command class defines the interface (execute and undo) but doesn’t implement them; IncrementCommand extends it to become a ConcreteCommand that knows how to modify a specific Receiver (state) by incrementing and decrementing its value by a given amount; the Client creates a command instance and the Invoker (here, just direct calls in client code) triggers execute() to increase the state and undo() to roll it back, showing how commands encapsulate actions and make undo/redo behavior explicit.

js
class Command {
  execute() { throw new Error('Not implemented'); }
  undo() { throw new Error('Not implemented'); }
}

class IncrementCommand extends Command {
  constructor(receiver, amount = 1) {
    super();
    this.receiver = receiver;
    this.amount = amount;
  }

  // Executes the command by increasing the receiver's value
  execute() { this.receiver.value += this.amount; }

  // Undoes the command by reversing the increment
  undo() { this.receiver.value -= this.amount; }
}

// Client code
const state = { value: 0 };
const cmd = new IncrementCommand(state, 5);
cmd.execute();
console.log(state.value); // 5
cmd.undo();
console.log(state.value); // 0

3.3 Async Command Variant

These are commands that perform asynchronous work (like API calls) fit naturally in modern JavaScript. The code below defines an asynchronous Command pattern variant where AsyncCommand wraps an async function (fn) and exposes a standardized execute() method that always returns a promise. Instead of directly calling the function, the client encapsulates it inside the command object, which allows uniform handling of async tasks (like queuing, retries, or logging). In the example, the command wraps a fetch call to an API endpoint; when asyncCmd.execute() is called, it executes the fetch, waits for the response, and returns the parsed JSON, showing how async commands can make asynchronous operations pluggable, reusable, and easier to orchestrate.

js
class AsyncCommand {
  constructor(fn) { this.fn = fn; }

  // The execute method returns a promise
  async execute() { return await this.fn(); }
}

// Example usage
const asyncCmd = new AsyncCommand(async () => {
  const res = await fetch('/api/do-work');
  return res.json();
});

await asyncCmd.execute();

4. Ways to use command pattern

Now that we have reached to a point of understanding the command pattern let us look at when is it suitable to use it and when it is not.

Use it when:

  • You need to decouple the invoker (UI, adapter, scheduler) from the logic that executes the request.
  • You want to serialize/deserialize actions (for persistence, network transport, undo/redo).
  • You need to queue, schedule, or retry operations.
  • You want to support macro commands (batch operations), logging, or undo/redo.

Avoid it when:

  • The codebase is small and command objects add unnecessary indirection.
  • Operations are pure one-off function calls with no lifecycle or orchestration needs.

5. Real-world use cases for command design pattern.

5.1 Undo / Redo Stack

To enable undo/redo functionality (common in editors and games), commands are tracked in stacks.

js
class CommandManager {
  constructor() { this.undoStack = []; this.redoStack = []; }

  // Executes a command and tracks it if undoable
   execute(cmd) {
    const result = await cmd.execute();
    if (cmd.undo) {
      this.undoStack.push(cmd);
      this.redoStack = []; // Clear redo chain on new action
    }
    return result;
  }

  // Undoes the last executed command
  undo() {
    const cmd = this.undoStack.pop();
    if (!cmd || !cmd.undo) return;
    cmd.undo();
    this.redoStack.push(cmd);
  }

  // Redoes the last undone command
  redo() {
    const cmd = this.redoStack.pop();
    if (!cmd) return;
    cmd.execute();
    this.undoStack.push(cmd);
  }
}

// Client code
const manager = new CommandManager();
const state = { value: 0 };
const inc = new IncrementCommand(state, 2);

manager.execute(inc);
console.log(state.value); // 2

manager.undo();
console.log(state.value); // 0

5.2 Macros (Composite Commands)

A MacroCommand groups multiple commands together so they can be executed and undone as a unit. In the code below the MacroCommand class is a composite command: instead of representing a single action, it groups multiple commands together and treats them as one. When execute() is called, it sequentially executes all contained commands; when undo() is called, it undoes them in reverse order, preserving logical consistency (for example, adding +5 then +10 should be undone as -10 then -5). In the client example, two IncrementCommand instances are created with a shared receiver (state). The client then wraps them in a MacroCommand, which lets you run them as a single atomic unit — this could be perfect for batch actions in editors, transactions, or scripting scenarios.

js
class MacroCommand {
  constructor(commands = []) { 
    this.commands = commands; 
  }

  // Executes each command in sequence
  execute() {
    for (const c of this.commands) c.execute();
  }

  // Undoes each command in reverse order (important for consistency)
  undo() {
    for (const c of [...this.commands].reverse()) {
      if (c.undo) c.undo();
    }
  }
}

// Example concrete command
class IncrementCommand {
  constructor(receiver, amount = 1) {
    this.receiver = receiver;
    this.amount = amount;
  }

  // Executes the command by incrementing receiver.value
  execute() { this.receiver.value += this.amount; }

  // Reverses the action by decrementing receiver.value
  undo() { this.receiver.value -= this.amount; }
}

// ----------------------
// Client code
// ----------------------
const state = { value: 0 };

const inc5 = new IncrementCommand(state, 5);
const inc10 = new IncrementCommand(state, 10);

// Client wires multiple commands into a MacroCommand
const macro = new MacroCommand([inc5, inc10]);

macro.execute();
console.log(state.value); // 15

macro.undo();
console.log(state.value); // 0

5.3 Logging & Event Sourcing

Commands can be serialized and stored, which allows for replaying them later (useful for event sourcing and audits). In the code below we define a SerializableCommand class, which is a special kind of command designed to be saved, transmitted, and later reconstructed. Each command is tagged with a type and a payload. The toJSON() method serializes it into a plain object that can be stored (e.g., in a database or sent over the network). The static fromJSON() method does the reverse: it takes serialized data and a registry (a lookup map of command type names to constructors), then re-instantiates the proper command object. This pattern is very useful in systems that need persistence, replay, or distributed execution of commands (for example: collaborative editors, event sourcing, or CQRS)

js
class SerializableCommand {
  constructor(type, payload) {
    this.type = type;
    this.payload = payload;
  }

  // Convert to JSON for persistence
  toJSON() {
    return { type: this.type, payload: this.payload };
  }

  // Reconstruct a command from JSON using a registry of constructors
  static fromJSON({ type, payload }, registry) {
    const ctor = registry[type];
    if (!ctor) throw new Error(`Unknown command type: ${type}`);
    return new ctor(payload);
  }
}

// ----------------------
// Example concrete command
// ----------------------
class IncrementCommand extends SerializableCommand {
  constructor(amount) {
    super('IncrementCommand', { amount });
  }

  execute(receiver) {
    receiver.value += this.payload.amount;
  }
}

// ----------------------
// Client code
// ----------------------
const state = { value: 0 };

// Registry of known commands
const registry = { IncrementCommand };

// Client creates a command
const cmd = new IncrementCommand(5);

// Serialize (e.g., save to DB or send over network)
const serialized = JSON.stringify(cmd.toJSON());

// Later, deserialize and replay
const parsed = JSON.parse(serialized);
const revivedCmd = SerializableCommand.fromJSON(parsed, registry);

// Execute deserialized command
revivedCmd.execute(state);

console.log(state.value); // 5

6. Command Bus Deep Dive

At scale, commands are dispatched via a Command Bus — a central mechanism that routes commands to handlers, often with middleware for logging, retries, and monitoring. This is crucial in enterprise apps and CQRS systems.

6.1 Why a Command Bus?

  • Decoupling: Clients only send commands; the bus finds the handler.
  • Cross-cutting concerns: Middleware can add logging, metrics, security, retries.
  • Asynchronous execution: Commands can be queued and executed later.
  • CQRS alignment: Commands express intent; events capture the facts.

6.2 Minimal Command Bus in JavaScript

This CommandBus implementation here acts as a central dispatcher for commands, decoupling the sender from the logic that executes them. Handlers are registered per command type, and middleware can be layered in to provide cross-cutting concerns like logging, retries, or authorization. When you call dispatch(command, ctx), the bus runs the command through the middleware pipeline (similar to Express middleware or Koa), and eventually invokes the registered handler for that command type. This pattern is powerful in larger systems (like CQRS or event-driven architectures), because it standardizes how commands are routed and processed.

js
// Example command
class CreateUserCommand {
  constructor(username) {
    this.username = username;
  }
}

// Example handler
const createUserHandler = async (command, ctx) => {
  ctx.db.push({ username: command.username });
  return { success: true };
};

// Example middleware for logging
const loggingMiddleware = async (command, ctx, next) => {
  console.log(`Dispatching: ${command.constructor.name}`);
  const result = await next();
  console.log(`Finished: ${command.constructor.name}`);
  return result;
};

// ----------------------
// Client code
// ----------------------
const bus = new CommandBus();

// Register a handler for CreateUserCommand
bus.register('CreateUserCommand', createUserHandler);

// Add logging middleware
bus.use(loggingMiddleware);

// Context holds shared dependencies (like a DB)
const ctx = { db: [] };

// Dispatch a command
(async () => {
  await bus.dispatch(new CreateUserCommand('alice'), ctx);
  console.log(ctx.db); // [{ username: 'alice' }]
})();

Above, the client is the code wiring everything together (registering handlers, adding middleware, and dispatching commands). The bus takes care of orchestration, so the client doesn’t need to know which handler actually runs.

6.3 Middleware Examples

Middleware enriches command execution with extra features. In the code below we define two reusable middleware functions for a CommandBus: a logging middleware and a retry middleware. The loggingMiddleware wraps command execution with before-and-after log statements, making it easier to trace what commands are running. The retryMiddleware adds resilience by catching errors when a command fails, then retrying execution up to a configurable number of attempts; in this case it retries with a simple loop (and could be extended with exponential backoff for production). Together, these middlewares demonstrate how cross-cutting concerns like observability and fault-tolerance can be applied transparently, without modifying individual command handlers. Lets look at the code:

js
// Example command
class FailingCommand {
  constructor() { this.attempts = 0; }
  async execute() {
    this.attempts++;
    if (this.attempts < 3) throw new Error("Failing...");
    return "Success!";
  }
}

// Command handler wraps the command’s execute()
const failingCommandHandler = async (cmd) => cmd.execute();

// Middleware definitions
const loggingMiddleware = async (cmd, ctx, next) => {
  console.log(`Executing ${cmd.constructor.name}`);
  const result = await next();
  console.log(`Finished ${cmd.constructor.name}`);
  return result;
};

// Retries failed commands up to `maxRetries` times
const retryMiddleware = (maxRetries = 3) => async (cmd, ctx, next) => {
  let attempt = 0;
  while (true) {
    try {
      return await next();
    } catch (err) {
      if (++attempt >= maxRetries) throw err;
      console.warn(`Retrying ${cmd.constructor.name}, attempt ${attempt}`);
    }
  }
};

// ----------------------
// Client code
// ----------------------
const bus = new CommandBus();
bus.register('FailingCommand', failingCommandHandler);

// Plug in middlewares: retry first, then logging
bus.use(retryMiddleware(5));
bus.use(loggingMiddleware);

(async () => {
  const result = await bus.dispatch(new FailingCommand(), {});
  console.log("Final Result:", result); // "Success!" after retries
})();

Above, the client sets up the bus, registers the handler, and attaches middlewares. The FailingCommand intentionally fails twice before succeeding, so the retry middleware ensures it eventually succeeds, while logging shows the execution lifecycle.

6.4 Async Dispatch with Queues

For distributed systems, commands may be serialized and placed into queues (Kafka, SQS, etc.), consumed later by workers. In the code below we define an AsyncCommandBus, which extends a normal CommandBus but adds asynchronous queuing support, making it suitable for distributed systems. Instead of executing commands immediately in memory, the dispatch method serializes the command (turning it into JSON) and publishes it to a message queue (e.g., RabbitMQ, Kafka, AWS SQS). Later, a worker or consumer service calls consume, which deserializes the message back into a command object and passes it to the base CommandBus’s dispatch method. This design enables asynchronous, decoupled command execution—the producer doesn’t wait for results, and commands can be processed by other services, retried, or scaled out across many workers.

js
// Mock queue implementation for demo purposes
const queue = {
  messages: [],
  async publish(topic, message) {
    console.log(`[Queue] Published to ${topic}:`, message);
    this.messages.push({ topic, message });
  },
  async consumeAll(consumerFn) {
    while (this.messages.length > 0) {
      const { message } = this.messages.shift();
      await consumerFn(message);
    }
  }
};

// AsyncCommandBus extending CommandBus
class AsyncCommandBus extends CommandBus {
  async dispatch(command, ctx) {
    const serialized = JSON.stringify(command);
    await queue.publish('commands', serialized); // push into queue
  }

  async consume(message) {
    const command = JSON.parse(message);
    await super.dispatch(command, {}); // normal dispatch via parent bus
  }
}

// Example command and handler
class SendEmailCommand {
  constructor(email, body) {
    this.email = email;
    this.body = body;
  }
}

const sendEmailHandler = async (cmd) => {
  console.log(` Sending email to ${cmd.email}: ${cmd.body}`);
};

// ----------------------
// Client code
// ----------------------
const bus = new AsyncCommandBus();
bus.register('SendEmailCommand', sendEmailHandler);

(async () => {
  // Producer side: dispatch adds the command to the queue
  await bus.dispatch(new SendEmailCommand('user@example.com', 'Welcome!'));

  // Consumer side: worker consumes and executes the command
  await queue.consumeAll(async (msg) => bus.consume(msg));
})();

Above, the client is the code wiring everything together: registering the handler, dispatching a SendEmailCommand, and consuming messages from the queue. The AsyncCommandBus ensures commands are serialized for transport, decoupling producers from consumers so work can be distributed, retried, or scaled across multiple workers.

6.5 CQRS and Command Bus

  • Write side: Commands update system state.
  • Read side: Events build query models.
  • Bus role: Standardizes validation, authorization, logging, routing.

6.6 more insights

  • Commands should be immutable for safe replay.
  • Middleware supports policies like distributed tracing and idempotency.
  • Collaborative apps benefit from deterministic ordering through a bus.
  • Dead-letter queues are essential in async buses for failed commands.

finally

INFO

The Command pattern is a powerful way to turn behavior into first-class objects. Its real strength lies in operational resilience: retries, idempotency, observability, transaction composition, and auditability. With the addition of a Command Bus, commands evolve from local design constructs into distributed, enterprise-grade primitives that power CQRS, event-sourced systems, and resilient architectures.


javascript book

If this interested you, check out my Javascript Book

Enjoy every byte.