The Command Pattern in Ruby

· 6 min read · Updated April 1, 2026 · intermediate
ruby command-pattern design-patterns solid guides

The command pattern is a behavioral design pattern that encapsulates a request as an object. Instead of sending a direct instruction to an object, you package that instruction along with all its data into a dedicated command object. This simple idea provides a range of powerful capabilities: undo/redo history, deferred execution, macro commands, and command queuing.

In this guide you will learn how the command pattern works in Ruby, how to build a clean command interface, implement concrete commands, add undo/redo support, compose macro commands, and see practical examples in CLI applications.

What Problem Does the Command Pattern Solve?

Consider a simple text editor with a Document class. You might have methods like document.bold_text, document.italicize_text, and document.underline_text. Callers invoke these methods directly, which works fine until you need any of the following:

  • Undo: Revert the last change
  • Redo: Re-apply a reverted change
  • Queue: Batch operations to run later
  • Logging: Record every action for auditing
  • Macro: Combine multiple operations into one

Direct method calls carry no context about what was done or how to reverse it. The command pattern solves this by representing each operation as an object with a standardized interface.

The Command Interface

At its core, a command is any object that responds to #execute. Some implementations also include #undo for reversible commands. Here is a minimal protocol:

module Command
  def execute
    raise NotImplementedError, "#{self.class} must implement #execute"
  end

  def undo
    raise NotImplementedError, "#{self.class} must implement #undo"
  end
end

This is not a Ruby module you include — it is a description of the interface contract. Concrete commands must implement both methods.

Concrete Commands

A concrete command ties a receiver (the object that does the actual work) to the action and its inverse. Here is a document editing example:

class BoldTextCommand
  attr_reader :document, :selection_start, :selection_end

  def initialize(document, selection_start, selection_end)
    @document = document
    @selection_start = selection_start
    @selection_end = selection_end
  end

  def execute
    document.make_bold(selection_start, selection_end)
  end

  def undo
    document.remove_bold(selection_start, selection_end)
  end
end

The BoldTextCommand knows which document to act on and which range to bold. It does not know about the rest of the editor. The caller (often an invoker) holds the command and decides when to call #execute or #undo.

The Invoker

The invoker is the object that triggers commands. It does not know the details of any command — it only knows the command interface.

class Editor
  def initialize
    @document = Document.new
    @history = CommandHistory.new
  end

  def bold_selection(start, finish)
    command = BoldTextCommand.new(@document, start, finish)
    @history.execute(command)
  end

  def undo
    @history.undo_last
  end

  def redo
    @history.redo_last
  end
end

Command History — Undo and Redo

Undo and redo require a history object that tracks executed commands. The simplest version maintains two stacks: one for undo and one for redo.

class CommandHistory
  def initialize
    @undo_stack = []
    @redo_stack = []
  end

  def execute(command)
    command.execute
    @undo_stack << command
    @redo_stack.clear  # New action clears the redo stack
  end

  def undo_last
    return if @undo_stack.empty?

    command = @undo_stack.pop
    command.undo
    @redo_stack << command
  end

  def redo_last
    return if @redo_stack.empty?

    command = @redo_stack.pop
    command.execute
    @undo_stack << command
  end
end

Each command must know how to undo itself. When a new command executes, the redo stack clears because redo history becomes irrelevant after a new action.

Composite Commands

Sometimes you want to treat a group of commands as a single unit. The composite command pattern wraps multiple commands behind a single interface.

class CompositeCommand
  def initialize
    @commands = []
  end

  def add(command)
    @commands << command
  end

  def execute
    @commands.each(&:execute)
  end

  def undo
    @commands.reverse.each(&:undo)
  end

  def size
    @commands.size
  end
end

You can now build complex operations from simple ones:

class FormatDocumentCommand < CompositeCommand
  def initialize(document, range)
    super()
    @document = document
    @range = range

    add(BoldTextCommand.new(document, range))
    add(ItalicizeTextCommand.new(document, range))
    add(UnderlineTextCommand.new(document, range))
  end
end

Calling FormatDocumentCommand#execute applies bold, italic, and underline in sequence. Calling #undo reverses them in the opposite order.

Macro Commands

Macro commands are composite commands with a specific purpose: representing a named sequence of actions that the user triggers as one. They are particularly useful in CLI applications where a single command might map to a multi-step workflow.

Command Queue

A command queue decouples when a command is created from when it runs. You enqueue commands and process them later, perhaps asynchronously or on a schedule.

class CommandQueue
  def initialize
    @queue = []
  end

  def enqueue(command)
    @queue << command
  end

  def execute_all
    @queue.each(&:execute)
  end

  def clear
    @queue.clear
  end

  def size
    @queue.size
  end
end

A practical use: a CLI tool that accepts multiple operations and runs them in order after parsing all flags and arguments.

queue = CommandQueue.new

ARGV.each do |arg|
  case arg
  when '--build'
    queue.enqueue(BuildCommand.new)
  when '--test'
    queue.enqueue(TestCommand.new)
  when '--deploy'
    queue.enqueue(DeployCommand.new)
  end
end

queue.execute_all

Using Ruby’s Proc as Commands

Ruby blocks and procs are objects, which means they can serve as lightweight commands without a full class. For simple one-off actions, this avoids class boilerplate.

class TaskRunner
  def initialize
    @tasks = []
  end

  def on_execute(&block)
    @tasks << block
  end

  def run
    @tasks.each(&:call)
  end
end

runner = TaskRunner.new
runner.on_execute { puts "Step 1" }
runner.on_execute { puts "Step 2" }
runner.on_execute { puts "Step 3" }
runner.run
# => Step 1
# => Step 2
# => Step 3

The proc approach works well for simple cases, but it has limits. Procs do not carry their own #undo method, so you lose undo/redo capability. For reversible operations, explicit command classes remain the better choice.

You can also combine procs with command objects for logging or timing:

class TimedCommand
  def initialize(command)
    @command = command
    @start_time = nil
    @end_time = nil
  end

  def execute
    @start_time = Time.now
    @command.execute
    @end_time = Time.now
    puts "Executed in #{@end_time - @start_time}s"
  end

  def undo
    @command.undo
  end
end

Practical CLI Example

Here is a complete, self-contained example showing the command pattern in a file processing CLI tool:

# Command interface
class RenameFileCommand
  def initialize(old_name, new_name)
    @old_name = old_name
    @new_name = new_name
  end

  def execute
    File.rename(@old_name, @new_name)
  end

  def undo
    File.rename(@new_name, @old_name)
  end
end

class DeleteFileCommand
  def initialize(filename)
    @filename = filename
    @content = File.read(filename)
  end

  def execute
    File.delete(@filename)
  end

  def undo
    File.write(@filename, @content)
  end
end

# Invoker with history
class FileManager
  def initialize
    @history = []
    @redo_stack = []
  end

  def execute_command(command)
    command.execute
    @history << command
    @redo_stack.clear
  end

  def undo
    return if @history.empty?
    command = @history.pop
    command.undo
    @redo_stack << command
  end

  def redo
    return if @redo_stack.empty?
    command = @redo_stack.pop
    command.execute
    @history << command
  end
end

# Usage
manager = FileManager.new
manager.execute_command(RenameFileCommand.new('report.txt', 'report_final.txt'))
manager.execute_command(DeleteFileCommand.new('temp.log'))
puts "Actions: #{manager.instance_variable_get(:@history).size}"  # => 2
manager.undo  # Restores temp.log and removes report_final.txt rename
manager.redo  # Re-applies both operations

This pattern scales to complex file workflows where every operation is reversible and auditable.

When to Use the Command Pattern

The command pattern is a strong fit when:

  • You need undo/redo functionality
  • Operations should be queueable or schedulable
  • You want to log, audit, or replay actions
  • Decoupling the object that initiates an action from the object that performs it matters
  • Macros (combining multiple operations into one) are needed

It adds some boilerplate — each action needs its own command class — so for simple, one-off operations that never need undo or queuing, a plain method call is usually simpler.

See Also