Greetings, traveler!
At first glance, the Command pattern felt like an academic thing. Something you learn once, then forget. That impression doesn’t survive real projects. Sooner or later, you’ll need to model actions as things: queue them, replay them, store them, undo them, sync them, log them, batch them. That’s what Command is about.
What problem does Command solve?
Most code uses direct calls:
- user taps a button
- you call a method
- the system changes state
Simple, fast, and usually fine.
But direct calls become limiting when you need to treat an action as data. For example:
- you want to queue actions and execute later
- you want to persist actions and replay them (automation, macros)
- you want to record user actions for analytics
- you want undo and redo
- you want to decouple “who triggers the action” from “who performs it”
Command gives you that decoupling. You wrap an action into a separate object. Now it can be stored, passed around, delayed, retried, combined, or reversed.
Command in one sentence
Command is a behavioral pattern that encapsulates a request as an object.
If the request is an object, it has identity. It can be placed in an array. It can be serialized. It can be logged. It can implement undo.
The core structure
Classic Command usually has these roles:
- Command: an interface describing the action
- Concrete Command: a specific action implementation
- Receiver: a component that does the work
- Invoker: something that triggers and stores commands
- Client: the code that wires everything together
You don’t need to force all these names into your codebase, but the roles are useful. They keep the pattern honest.
Let’s build a small example that uses all of them.
Example: racing game commands
Imagine a racing game where the player controls a car. Instead of calling methods directly, we model actions as commands.
We will support:
- move forward
- move backward
- turn left
- turn right
- execute a series of actions
- undo already executed actions
Receiver
Receiver is the object that knows how to perform the actual work.
In this example, it is a Car.
final class Car {
enum Direction: String {
case north, east, south, west
}
let model: String
private(set) var direction: Direction = .north
private(set) var distance: Int = 0
init(model: String) {
self.model = model
}
func moveForward() {
distance += 1
print("\(model) moves forward. Distance: \(distance)")
}
func moveBackward() {
distance = max(0, distance - 1)
print("\(model) moves backward. Distance: \(distance)")
}
func turnLeft() {
direction = switch direction {
case .north: .west
case .west: .south
case .south: .east
case .east: .north
}
print("\(model) turns left. Direction: \(direction.rawValue)")
}
func turnRight() {
direction = switch direction {
case .north: .east
case .east: .south
case .south: .west
case .west: .north
}
print("\(model) turns right. Direction: \(direction.rawValue)")
}
}This car has state. It changes over time. That’s important, because it gives undo something real to work with.
Command interface
Command should describe two operations:
- execute
- undo
protocol CarCommand {
func execute()
func undo()
}This is the key change compared to the “queued actions” version. Without undo(), you don’t have undo. You just have a list of planned actions.
Concrete commands
Each concrete command stores the receiver and implements both operations.
final class MoveForward: CarCommand {
private let car: Car
init(car: Car) {
self.car = car
}
func execute() {
car.moveForward()
}
func undo() {
car.moveBackward()
}
}
final class MoveBackward: CarCommand {
private let car: Car
init(car: Car) {
self.car = car
}
func execute() {
car.moveBackward()
}
func undo() {
car.moveForward()
}
}
final class TurnLeft: CarCommand {
private let car: Car
init(car: Car) {
self.car = car
}
func execute() {
car.turnLeft()
}
func undo() {
car.turnRight()
}
}
final class TurnRight: CarCommand {
private let car: Car
init(car: Car) {
self.car = car
}
func execute() {
car.turnRight()
}
func undo() {
car.turnLeft()
}
}Notice what changed here.
We didn’t pass car into execute. The command already owns the receiver. That’s one of the best parts of this pattern: you can schedule or store the command without needing extra context later.
Invoker
Invoker is responsible for executing commands and keeping history.
History is what makes undo possible.
final class RacingGame {
private var history: [CarCommand] = []
func perform(_ command: CarCommand) {
command.execute()
history.append(command)
}
func undoLast() {
guard let last = history.popLast() else { return }
last.undo()
}
func undoAll() {
while let last = history.popLast() {
last.undo()
}
}
}This version performs commands immediately.
That choice is intentional. For real undo, you need a record of actions that already happened. Otherwise you’re just editing a plan.
Usage
let game = RacingGame()
let car = Car(model: "BMW Z3")
game.perform(MoveForward(car: car))
game.perform(MoveForward(car: car))
game.perform(TurnRight(car: car))
game.perform(MoveForward(car: car))
game.undoLast()
game.undoLast()If you run it, you’ll see that:
- commands modify the state
- undo reverses the last executed command
- the car goes back to the previous state step-by-step
That is actual undo.
Undo is not “remove last planned action”
A common misunderstanding is to implement rewind like this:
- store commands in an array
- remove the last one
- call it “undo”
That is not undo. Undo means:
- an action was executed
- state changed
- now you reverse it
And the easiest way to implement it is exactly what we did:
- command knows how to execute
- command knows how to undo
- invoker keeps a history
There is no shortcut around that.
Macro commands
Command becomes more useful when actions can be grouped.
A macro command is just a command that contains other commands.
final class MacroCommand: CarCommand {
private let commands: [CarCommand]
init(commands: [CarCommand]) {
self.commands = commands
}
func execute() {
commands.forEach { $0.execute() }
}
func undo() {
commands.reversed().forEach { $0.undo() }
}
}A macro command becomes a reusable scenario:
- drift around a corner
- parking sequence
- tutorial demo
- replay system
Where this pattern shows up in real apps
Command is easy to miss because nobody names things “Command” in production. But the structure shows up all the time:
- undo/redo in editors
- operation queues with retries
- batching user actions for sync
- user automation
- UI actions that need to be logged, replayed, or tested
Once you start thinking in commands, a lot of problems become simpler.
Conclusion
Command is a practical tool for when an action should be treated like a value. If your app needs history, batching, replay, or undo, this pattern can be useful. It becomes the cleanest way to keep control over behavior without turning your business code into a pile of flags.
We have finished exploring the Command pattern and will move on to the next design pattern — the Iterator.
