StackPractices
intermediate By Mathias Paulenko

Command Pattern

Encapsulate a request as an object, letting you parameterize clients with queues, logs, and undoable operations. A behavioral design pattern.

Topics: design

Overview

The Command Pattern is a behavioral design pattern that turns a request into a stand-alone object containing all information about the request. This lets you parameterize methods with different requests, delay or queue execution, and support undoable operations.

It is the basis for undo/redo systems, job queues, macro recording, and transactional operations.

When to Use

Use the Command Pattern when:

  • You need to parameterize objects with operations to execute
  • You want to queue, schedule, or execute operations remotely
  • You need undo/redo functionality
  • You want to log changes for replay or audit purposes
  • You need transactional behavior (execute all or roll back)

Solution

Python

from abc import ABC, abstractmethod

class Command(ABC):
    @abstractmethod
    def execute(self):
        pass

    @abstractmethod
    def undo(self):
        pass

class Light:
    def __init__(self):
        self.is_on = False

    def turn_on(self):
        self.is_on = True
        print("Light is on")

    def turn_off(self):
        self.is_on = False
        print("Light is off")

class TurnOnCommand(Command):
    def __init__(self, light: Light):
        self.light = light

    def execute(self):
        self.light.turn_on()

    def undo(self):
        self.light.turn_off()

# Usage
light = Light()
cmd = TurnOnCommand(light)
cmd.execute()  # Light is on
cmd.undo()     # Light is off

JavaScript

class Light {
  constructor() {
    this.isOn = false;
  }
  turnOn() {
    this.isOn = true;
    console.log("Light is on");
  }
  turnOff() {
    this.isOn = false;
    console.log("Light is off");
  }
}

class TurnOnCommand {
  constructor(light) {
    this.light = light;
  }
  execute() {
    this.light.turnOn();
  }
  undo() {
    this.light.turnOff();
  }
}

// Usage
const light = new Light();
const cmd = new TurnOnCommand(light);
cmd.execute(); // Light is on
cmd.undo();    // Light is off

Java

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

class Light {
    boolean isOn = false;
    void turnOn() { isOn = true; System.out.println("Light is on"); }
    void turnOff() { isOn = false; System.out.println("Light is off"); }
}

class TurnOnCommand implements Command {
    private final Light light;
    TurnOnCommand(Light light) { this.light = light; }
    public void execute() { light.turnOn(); }
    public void undo() { light.turnOff(); }
}

// Usage
Light light = new Light();
Command cmd = new TurnOnCommand(light);
cmd.execute(); // Light is on
cmd.undo();    // Light is off

Explanation

The Command Pattern separates action invocation from execution:

  • Command Interface: Declares execute() and optionally undo()
  • Concrete Command (TurnOnCommand): Binds a receiver (Light) to an action (turnOn)
  • Receiver (Light): The object that performs the actual work
  • Invoker: Calls execute() on commands (e.g., a button, scheduler, or remote control)

By encapsulating requests as objects, you gain the ability to queue, log, and reverse operations.

Variants

VariantUse CaseTrade-off
Simple CommandDirect action with no undoEasy to implement, limited flexibility
Undoable CommandOperations that can be reversedRequires maintaining state for reversal
Macro CommandComposite of multiple commandscapable, but harder to undo atomically

What Works

  • Implement undo() for every command if your system supports undo
  • Keep commands stateless when possible: Store receiver state, not command state
  • Use a command history (stack) to support multi-level undo/redo
  • Document side effects: Commands that affect external systems are harder to undo
  • Consider immutability: Once configured, a command should not change its target

Common Mistakes

  • Forgetting undo state: Commands that cannot be reversed break the undo stack
  • Tight coupling: Commands that depend on global state instead of a specific receiver
  • Over-engineering: Using Command for trivial, one-off operations that never need queuing or undo
  • Synchronous assumptions: Not considering that commands may be executed asynchronously
  • Missing idempotency: Running the same command twice produces different results
  • Not handling command failures: Commands that throw exceptions can leave the system in an inconsistent state
  • Storing too much state in commands: Commands should be lightweight; storing large objects affects memory and serialization
  • Ignoring thread safety: Commands executed concurrently may access shared resources without proper synchronization
  • Not logging command execution: Missing audit trails make debugging and compliance difficult
  • Mixing concerns: Commands that perform multiple unrelated responsibilities violate single responsibility principle

Best Practices

  1. Keep commands lightweight. Commands should be small, focused objects that encapsulate a single action. Avoid storing large amounts of data in commands.

  2. Implement undo for all state-changing commands. If your system supports undo, every command that changes state should implement undo logic.

  3. Use command factories for complex creation. When commands require complex setup, use factory methods or builders to encapsulate creation logic.

  4. Handle exceptions gracefully. Commands should handle their own exceptions or provide clear error information to the invoker.

  5. Make commands serializable. If you need to persist commands or send them over the network, ensure they can be serialized and deserialized.

  6. Document command side effects. Clearly document any external side effects a command may have, especially those that are difficult to undo.

  7. Use command history for debugging. Maintain a history of executed commands to help with debugging and audit trails.

  8. Consider command composition. Use composite commands to build complex operations from simpler, reusable commands.

  9. Validate command parameters. Validate command parameters before execution to fail fast and provide clear error messages.

  10. Test commands in isolation. Write unit tests for each command independently, then integration tests for command chains and macros.

Frequently Asked Questions

What is the difference between Command and Strategy?

Strategy encapsulates interchangeable algorithms. Command encapsulates a request to perform an action, often with support for undo, queuing, and logging.

Can Command be used without undo?

Yes. The undo capability is optional. Many systems use Command solely for queuing and decoupling invokers from receivers.

How do I implement multi-level undo?

Maintain a stack of executed commands. Undo pops the stack and calls undo(). See Command with Undo/Redo for a full implementation. Redo pushes the command back and calls execute().

Can commands be executed asynchronously?

Yes. Implement async command interfaces with execute() and undo() methods that return promises or use async/await patterns. This is useful for I/O-bound operations like database calls or network requests.

How do I handle command failures?

Implement error handling at the command level or use command decorators that wrap execution with try-catch blocks. Consider implementing retry logic, circuit breaking, or fallback mechanisms for transient failures.

Should commands be serializable?

Yes, if you need to persist commands for replay, audit trails, or distributed execution. Ensure commands can be serialized to JSON, binary, or another format and deserialized without losing state.

How do I implement command queuing?

Use a queue data structure to hold commands and a worker thread or process to execute them. This enables deferred execution, background processing, and load balancing across workers.

Can commands be composed?

Yes. Use composite commands (macro commands) to combine multiple commands into a single executable unit. This is useful for complex operations that require multiple steps to be executed atomically.

How do I implement command logging?

Wrap commands with logging decorators that record execution details, parameters, and results. This provides audit trails and debugging information for command execution.

Should commands validate their parameters?

Yes. Validate command parameters before execution to fail fast and provide clear error messages. This prevents invalid commands from being executed and causing inconsistent state.