The Command Pattern in Ruby
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
- /guides/ruby-observer-pattern/ — Another Gang of Four behavioral pattern for broadcasting events
- /guides/ruby-service-objects/ — Service objects complement commands by organizing business logic