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
- /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