← Back to principles

Design Principle

Command Pattern

Learn the Command pattern: encapsulate a request as an object, allowing you to parameterize clients with different requests, queue operations, and support undo.

The Command Pattern encapsulates a request as an object, allowing you to parameterize clients with different requests, queue operations, log requests, and support undo operations.


What is the Command Pattern?

Command Pattern provides:

  • Request encapsulation: Encapsulate request as object
  • Decoupling: Decouple invoker from receiver
  • Undo/Redo: Support undo and redo operations
  • Queue operations: Queue and schedule operations
  • Logging: Log and audit operations

Use cases:

  • Undo/redo functionality
  • Macro recording
  • Transaction systems
  • Job queues
  • Remote procedure calls

Structure

Command (interface)
  └─ execute()
  └─ undo()

ConcreteCommand (implements Command)
  └─ receiver: Receiver
  └─ execute() (calls receiver.action())
  └─ undo() (reverses action)

Invoker
  └─ command: Command
  └─ execute() (calls command.execute())

Receiver
  └─ action()

Examples

Basic Command Pattern

// Command interface
interface Command {
  execute(): void;
  undo(): void;
}

// Receiver
class Light {
  private isOn: boolean = false;
  
  turnOn(): void {
    this.isOn = true;
    console.log("Light is ON");
  }
  
  turnOff(): void {
    this.isOn = false;
    console.log("Light is OFF");
  }
  
  getState(): boolean {
    return this.isOn;
  }
}

// Concrete commands
class TurnOnCommand implements Command {
  private light: Light;
  
  constructor(light: Light) {
    this.light = light;
  }
  
  execute(): void {
    this.light.turnOn();
  }
  
  undo(): void {
    this.light.turnOff();
  }
}

class TurnOffCommand implements Command {
  private light: Light;
  
  constructor(light: Light) {
    this.light = light;
  }
  
  execute(): void {
    this.light.turnOff();
  }
  
  undo(): void {
    this.light.turnOn();
  }
}

// Invoker
class RemoteControl {
  private command: Command | null = null;
  private history: Command[] = [];
  
  setCommand(command: Command): void {
    this.command = command;
  }
  
  pressButton(): void {
    if (this.command) {
      this.command.execute();
      this.history.push(this.command);
    }
  }
  
  undo(): void {
    if (this.history.length > 0) {
      const lastCommand = this.history.pop()!;
      lastCommand.undo();
    }
  }
}

// Usage
const light = new Light();
const turnOn = new TurnOnCommand(light);
const turnOff = new TurnOffCommand(light);

const remote = new RemoteControl();
remote.setCommand(turnOn);
remote.pressButton(); // Light is ON

remote.setCommand(turnOff);
remote.pressButton(); // Light is OFF

remote.undo(); // Light is ON (undo last command)

Text Editor with Undo/Redo

// Command interface
interface TextCommand {
  execute(): void;
  undo(): void;
}

// Receiver
class TextEditor {
  private content: string = "";
  
  insert(text: string, position: number): void {
    this.content = this.content.slice(0, position) + text + this.content.slice(position);
  }
  
  delete(position: number, length: number): string {
    const deleted = this.content.slice(position, position + length);
    this.content = this.content.slice(0, position) + this.content.slice(position + length);
    return deleted;
  }
  
  getContent(): string {
    return this.content;
  }
}

// Concrete commands
class InsertCommand implements TextCommand {
  private editor: TextEditor;
  private text: string;
  private position: number;
  
  constructor(editor: TextEditor, text: string, position: number) {
    this.editor = editor;
    this.text = text;
    this.position = position;
  }
  
  execute(): void {
    this.editor.insert(this.text, this.position);
  }
  
  undo(): void {
    this.editor.delete(this.position, this.text.length);
  }
}

class DeleteCommand implements TextCommand {
  private editor: TextEditor;
  private position: number;
  private length: number;
  private deletedText: string = "";
  
  constructor(editor: TextEditor, position: number, length: number) {
    this.editor = editor;
    this.position = position;
    this.length = length;
  }
  
  execute(): void {
    this.deletedText = this.editor.delete(this.position, this.length);
  }
  
  undo(): void {
    this.editor.insert(this.deletedText, this.position);
  }
}

// Command history
class CommandHistory {
  private history: TextCommand[] = [];
  private currentIndex: number = -1;
  
  execute(command: TextCommand): void {
    // Remove any commands after current index (when undoing and then doing new command)
    this.history = this.history.slice(0, this.currentIndex + 1);
    
    command.execute();
    this.history.push(command);
    this.currentIndex++;
  }
  
  undo(): void {
    if (this.canUndo()) {
      this.history[this.currentIndex].undo();
      this.currentIndex--;
    }
  }
  
  redo(): void {
    if (this.canRedo()) {
      this.currentIndex++;
      this.history[this.currentIndex].execute();
    }
  }
  
  canUndo(): boolean {
    return this.currentIndex >= 0;
  }
  
  canRedo(): boolean {
    return this.currentIndex < this.history.length - 1;
  }
}

// Usage
const editor = new TextEditor();
const history = new CommandHistory();

history.execute(new InsertCommand(editor, "Hello", 0));
history.execute(new InsertCommand(editor, " World", 5));
console.log(editor.getContent()); // "Hello World"

history.undo();
console.log(editor.getContent()); // "Hello"

history.redo();
console.log(editor.getContent()); // "Hello World"

Job Queue

// Command interface
interface Job {
  execute(): Promise<void>;
  getName(): string;
}

// Concrete commands
class EmailJob implements Job {
  constructor(
    private to: string,
    private subject: string,
    private body: string
  ) {}
  
  async execute(): Promise<void> {
    console.log(`Sending email to ${this.to}: ${this.subject}`);
    // Simulate email sending
    await new Promise(resolve => setTimeout(resolve, 1000));
    console.log(`Email sent to ${this.to}`);
  }
  
  getName(): string {
    return `Email to ${this.to}`;
  }
}

class DatabaseBackupJob implements Job {
  constructor(private database: string) {}
  
  async execute(): Promise<void> {
    console.log(`Backing up database: ${this.database}`);
    await new Promise(resolve => setTimeout(resolve, 2000));
    console.log(`Backup completed for ${this.database}`);
  }
  
  getName(): string {
    return `Backup ${this.database}`;
  }
}

// Invoker (Job Queue)
class JobQueue {
  private queue: Job[] = [];
  private isProcessing: boolean = false;
  
  addJob(job: Job): void {
    this.queue.push(job);
    console.log(`Job added: ${job.getName()}`);
    this.processQueue();
  }
  
  private async processQueue(): Promise<void> {
    if (this.isProcessing || this.queue.length === 0) {
      return;
    }
    
    this.isProcessing = true;
    
    while (this.queue.length > 0) {
      const job = this.queue.shift()!;
      try {
        await job.execute();
      } catch (error) {
        console.error(`Job failed: ${job.getName()}`, error);
      }
    }
    
    this.isProcessing = false;
  }
  
  getQueueSize(): number {
    return this.queue.length;
  }
}

// Usage
const queue = new JobQueue();

queue.addJob(new EmailJob("user@example.com", "Welcome", "Welcome to our service!"));
queue.addJob(new DatabaseBackupJob("production"));
queue.addJob(new EmailJob("admin@example.com", "Backup Complete", "Backup finished"));

Common Pitfalls

  • Command bloat: Too many command classes. Fix: Use parameterized commands
  • Memory usage: History can consume memory. Fix: Limit history size
  • Undo complexity: Complex undo logic. Fix: Store state snapshots
  • Not handling errors: Commands fail silently. Fix: Add error handling

Interview Questions

Beginner

Q: What is the Command pattern and when would you use it?

A:

Command Pattern encapsulates a request as an object, allowing you to parameterize clients with different requests.

Key characteristics:

  • Request encapsulation: Request becomes an object
  • Decoupling: Decouples invoker from receiver
  • Undo/Redo: Support undo and redo operations
  • Queue operations: Queue and schedule operations

Example:

interface Command {
  execute(): void;
  undo(): void;
}

class LightOnCommand implements Command {
  private light: Light;
  
  execute(): void {
    this.light.turnOn();
  }
  
  undo(): void {
    this.light.turnOff();
  }
}

Use cases:

  • Undo/redo: Text editors, graphics editors
  • Macro recording: Record and replay commands
  • Job queues: Queue operations for later execution
  • Transaction systems: Atomic operations

Benefits:

  • Decoupling: Invoker doesn't know receiver
  • Undo/redo: Easy to implement
  • Queue operations: Schedule commands
  • Logging: Log all commands

Intermediate

Q: Explain how the Command pattern supports undo/redo. How do you implement command history?

A:

Command Pattern for Undo/Redo:

1. Command Interface:

interface Command {
  execute(): void;
  undo(): void;
}

2. Concrete Command:

class InsertCommand implements Command {
  private editor: TextEditor;
  private text: string;
  private position: number;
  
  execute(): void {
    this.editor.insert(this.text, this.position);
  }
  
  undo(): void {
    this.editor.delete(this.position, this.text.length);
  }
}

3. Command History:

class CommandHistory {
  private history: Command[] = [];
  private currentIndex: number = -1;
  
  execute(command: Command): void {
    // Remove commands after current (when doing new command after undo)
    this.history = this.history.slice(0, this.currentIndex + 1);
    
    command.execute();
    this.history.push(command);
    this.currentIndex++;
  }
  
  undo(): void {
    if (this.canUndo()) {
      this.history[this.currentIndex].undo();
      this.currentIndex--;
    }
  }
  
  redo(): void {
    if (this.canRedo()) {
      this.currentIndex++;
      this.history[this.currentIndex].execute();
    }
  }
}

Key Points:

  • Store commands: Keep executed commands in history
  • Track index: Track current position in history
  • Remove future: When doing new command after undo, remove future commands
  • Execute/undo: Execute for redo, undo for undo

Senior

Q: Design a command system for a distributed transaction system that supports rollback, logging, and ensures commands are executed atomically across multiple services.

A:

// Command interface
interface TransactionCommand {
  execute(): Promise<void>;
  rollback(): Promise<void>;
  getServiceId(): string;
  getId(): string;
}

// Concrete commands
class TransferCommand implements TransactionCommand {
  private id: string;
  private serviceId: string = "payment-service";
  private fromAccount: string;
  private toAccount: string;
  private amount: number;
  private executed: boolean = false;
  
  constructor(fromAccount: string, toAccount: string, amount: number) {
    this.id = this.generateId();
    this.fromAccount = fromAccount;
    this.toAccount = toAccount;
    this.amount = amount;
  }
  
  async execute(): Promise<void> {
    // Deduct from source account
    await this.deduct(this.fromAccount, this.amount);
    
    // Credit to destination account
    await this.credit(this.toAccount, this.amount);
    
    this.executed = true;
  }
  
  async rollback(): Promise<void> {
    if (this.executed) {
      // Reverse the transaction
      await this.credit(this.fromAccount, this.amount);
      await this.deduct(this.toAccount, this.amount);
      this.executed = false;
    }
  }
  
  getServiceId(): string {
    return this.serviceId;
  }
  
  getId(): string {
    return this.id;
  }
  
  private async deduct(account: string, amount: number): Promise<void> {
    // Deduct logic
  }
  
  private async credit(account: string, amount: number): Promise<void> {
    // Credit logic
  }
  
  private generateId(): string {
    return `cmd-${Date.now()}-${Math.random()}`;
  }
}

// Transaction coordinator
class TransactionCoordinator {
  private commands: TransactionCommand[] = [];
  private executedCommands: TransactionCommand[] = [];
  private logger: TransactionLogger;
  
  constructor(logger: TransactionLogger) {
    this.logger = logger;
  }
  
  addCommand(command: TransactionCommand): void {
    this.commands.push(command);
  }
  
  async execute(): Promise<void> {
    // Two-phase commit
    try {
      // Phase 1: Prepare (validate all commands)
      await this.prepare();
      
      // Phase 2: Commit (execute all commands)
      await this.commit();
      
      this.logger.log("Transaction committed", this.commands);
    } catch (error) {
      // Rollback on failure
      await this.rollback();
      this.logger.log("Transaction rolled back", this.commands);
      throw error;
    }
  }
  
  private async prepare(): Promise<void> {
    // Validate all commands can execute
    for (const command of this.commands) {
      await this.validateCommand(command);
    }
  }
  
  private async commit(): Promise<void> {
    // Execute all commands
    for (const command of this.commands) {
      try {
        await command.execute();
        this.executedCommands.push(command);
      } catch (error) {
        // If any command fails, rollback all executed
        await this.rollbackExecuted();
        throw error;
      }
    }
  }
  
  private async rollback(): Promise<void> {
    // Rollback in reverse order
    for (let i = this.executedCommands.length - 1; i >= 0; i--) {
      try {
        await this.executedCommands[i].rollback();
      } catch (error) {
        // Log but continue rollback
        this.logger.logError(`Rollback failed for ${this.executedCommands[i].getId()}`, error);
      }
    }
    this.executedCommands = [];
  }
  
  private async rollbackExecuted(): Promise<void> {
    await this.rollback();
  }
  
  private async validateCommand(command: TransactionCommand): Promise<void> {
    // Validate command can execute
    // Check account exists, sufficient funds, etc.
  }
}

// Transaction logger
class TransactionLogger {
  log(message: string, commands: TransactionCommand[]): void {
    console.log(`${message}:`, commands.map(c => c.getId()));
    // Log to persistent storage
  }
  
  logError(message: string, error: any): void {
    console.error(message, error);
    // Log error to persistent storage
  }
}

// Usage
const coordinator = new TransactionCoordinator(new TransactionLogger());
coordinator.addCommand(new TransferCommand("acc1", "acc2", 100));
coordinator.addCommand(new TransferCommand("acc2", "acc3", 50));

try {
  await coordinator.execute();
  console.log("Transaction successful");
} catch (error) {
  console.error("Transaction failed:", error);
}

Features:

  1. Atomic execution: All commands execute or none
  2. Rollback: Rollback all commands on failure
  3. Logging: Log all transactions
  4. Two-phase commit: Prepare then commit
  5. Error handling: Handle failures gracefully

Key Takeaways

  • Command pattern: Encapsulates request as object
  • Decoupling: Decouples invoker from receiver
  • Undo/redo: Easy to implement with command history
  • Queue operations: Queue and schedule commands
  • Use cases: Undo/redo, job queues, transactions, macros
  • Best practices: Store state for undo, handle errors, limit history size

Keep exploring

Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.