Command Design Pattern in Swift


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.