rubyguides

The Command Pattern in Ruby: Encapsulating Actions as Reusable Objects

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.

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.

TL;DR

Use the command pattern when the action needs to become an object of its own. That extra object gives you undo, redo, logging, queueing, and reuse without scattering the logic across the codebase. In Ruby, the pattern fits especially well when the same operation needs to be replayed, delayed, or combined with other operations.

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.

The point of the interface is consistency. Once every command responds to the same messages, the invoker can stay simple and the command implementations can focus on their own receivers. That separation is what makes the pattern useful: the caller knows the shape of the interaction, but not the details of the work.

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. The undo stack holds every command that has been executed, and when you undo, the top of that stack gets reversed and pushed onto the redo stack instead. This double-stack pattern is the standard approach in most editors and CAD tools that implement undo.

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.

The two-stack design is the simplest correct implementation and appears in almost every editor and drawing tool that supports undo. If the undo stack is empty, there is nothing to reverse. If the redo stack is non-empty, a new action wipes it because the redo path diverges from the new action path.

Separating history tracking from command logic is what makes this pattern practical for real editing tools and admin workflows. A command history does not need to understand what the commands do; it only needs to remember the order in which they ran and ask them to reverse themselves when necessary. That keeps the history object small and the commands self-contained.

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

Composite commands let you build higher-level operations without changing any of the individual command classes. The inner commands stay focused on their own receivers, while the composite handles the group lifecycle. Undo also works cleanly here because each inner command knows how to reverse itself, and the composite just calls undo on each one in reverse order.

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 command queue is useful when the system needs to collect work first and act on it second. This pattern appears in build systems, deployment scripts, and any tool where flags should be parsed before side effects begin. By keeping the queue separate from the execution loop, you can also add retries, timeouts, or parallel execution later without changing the commands.

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. A proc command is just a callable stored for later, and because Ruby treats blocks as first-class objects, the syntax stays compact enough for quick scripts and callbacks.

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.

That tradeoff matters when the app needs auditability or replay. A proc is fine when you only want to run a small callback. A command object is better when the operation has identity, state, and a clear inverse. If you can explain the action in one sentence and that sentence includes a verb and an object, a command class is usually a good fit.

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. This example ties together command objects, history, and undo/redo in a context that mirrors real-world automation scripts. Each file operation is wrapped as a command so the tool can record, undo, and replay every step.

# 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

The two command classes above are self-contained: each one knows which file to act on, how to perform the action, and how to reverse it. That independence makes them easy to test in isolation before adding the invoker layer.

The invoker below wraps those commands in a history-tracking shell that handles the undo/redo lifecycle:

# 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

Command objects like these scale to complex file workflows where every operation is reversible and auditable.

CLI tools in particular benefit from command objects. A CLI often collects flags and arguments first, then decides what should happen afterward. Commands let you separate parsing from execution, so the parser does not have to know anything about the internals of each action.

When to use the command pattern

You should reach for the command pattern in these situations:

  • 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

The command pattern 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.

Wrapping actions in command objects is worth the extra structure when actions need to travel through time. If they must be queued, retried, logged, or undone, turning them into objects gives you a clean place to store that behavior. If none of those needs exist, the pattern is probably more machinery than the task deserves. For a complementary approach to organizing business logic, see the service objects guide.

See Also