Skip to content
Commitwise
Menu
Design Patterns

Command

Turn actions into objects you can store, undo, redo, queue and replay, from an editor's undo stack to an offline queue of changes that syncs later, and keep commands serializable and idempotent so they survive crashes and retries, in Swift, Kotlin, Java and TypeScript.

intermediate

The core idea with one simple example.Step by step, with more examples and exercises.A cheat sheet: key APIs, gotchas and trade-offs.Internals, trade-offs and production pitfalls.

A drawing app needs undo. A first attempt saves a copy of the whole canvas after every stroke: 200 strokes later, the app holds 200 full images and runs out of memory. A notes app needs to work offline: edits made on the subway must reach the server later, in order, even if the app is killed in between. Both problems have the same solution: stop executing actions directly, and turn each one into an object that knows how to do itself (and, for undo, how to reverse itself).

That object is a Command.

New to some of these? Open what you need, or skip ahead.

Basics:What is an interface (or protocol)?

An interface (Kotlin, Java, TypeScript) or protocol (Swift) is a list of things a type promises it can do, without saying how. Code that only knows the interface can use any class that implements it: a list of Commands can hold “add shape”, “move shape” and “delete shape” side by side.

Basics:What is a stack?

A stack is a list where you only add and remove at the top, like a pile of plates: the last one in is the first one out. Undo history is a stack: undo takes the most recent action off the top.

Part Not started

Actions as objects: undo and redo

A waiter writes your order on a slip instead of shouting it to the kitchen. The slip can wait in a queue, be handed to whichever cook is free, be crossed out if you change your mind, and tell the cashier later exactly what you ordered.

A command is that slip for your code: an object that describes one action (“move this shape 20 points right”) and can execute() it. For undo, it also knows how to undo() itself, because it remembers what it changed. The app keeps executed commands on an undo stack; pressing Undo takes the top one and reverses it.

The Command pattern encapsulates a request as an object, decoupling the invoker (a button, a keyboard shortcut, a queue) from the receiver that does the work, and making requests first-class: storable, composable (macro commands), loggable, undoable and replayable. Undo works either by inverse operations (each command knows its reverse) or by memento snapshots of the affected state; inverse operations are cheap but must capture exactly the state they overwrite, at execution time. Platform undo managers (UndoManager on Apple platforms, the browser’s editing history, IDE command stacks) are implementations of this.

protocol Command {
    func execute()
    func undo()
}

final class MoveShape: Command {
    private let canvas: Canvas
    private let shapeId: Shape.ID
    private let offset: CGVector

    init(canvas: Canvas, shapeId: Shape.ID, offset: CGVector) {
        self.canvas = canvas; self.shapeId = shapeId; self.offset = offset
    }
    func execute() { canvas.move(shapeId, by: offset) }
    func undo() { canvas.move(shapeId, by: CGVector(dx: -offset.dx, dy: -offset.dy)) }
}

final class History {
    private var undoStack: [Command] = []
    private var redoStack: [Command] = []

    func perform(_ command: Command) {
        command.execute()
        undoStack.append(command)
        redoStack.removeAll()                 // a new action makes redo history invalid
    }
    func undo() {
        guard let command = undoStack.popLast() else { return }
        command.undo()
        redoStack.append(command)
    }
    func redo() {
        guard let command = redoStack.popLast() else { return }
        command.execute()
        undoStack.append(command)
    }
}
interface Command {
    fun execute()
    fun undo()
}

class MoveShape(
    private val canvas: Canvas,
    private val shapeId: ShapeId,
    private val dx: Float,
    private val dy: Float,
) : Command {
    override fun execute() = canvas.move(shapeId, dx, dy)
    override fun undo() = canvas.move(shapeId, -dx, -dy)
}

class History {
    private val undoStack = ArrayDeque<Command>()
    private val redoStack = ArrayDeque<Command>()

    fun perform(command: Command) {
        command.execute()
        undoStack.addLast(command)
        redoStack.clear()                     // a new action makes redo history invalid
    }
    fun undo() {
        val command = undoStack.removeLastOrNull() ?: return
        command.undo()
        redoStack.addLast(command)
    }
    fun redo() {
        val command = redoStack.removeLastOrNull() ?: return
        command.execute()
        undoStack.addLast(command)
    }
}
public interface Command {
    void execute();
    void undo();
}

public final class MoveShape implements Command {
    private final Canvas canvas;
    private final ShapeId shapeId;
    private final float dx, dy;

    public MoveShape(Canvas canvas, ShapeId shapeId, float dx, float dy) {
        this.canvas = canvas; this.shapeId = shapeId; this.dx = dx; this.dy = dy;
    }
    public void execute() { canvas.move(shapeId, dx, dy); }
    public void undo() { canvas.move(shapeId, -dx, -dy); }
}

public final class History {
    private final Deque<Command> undoStack = new ArrayDeque<>();
    private final Deque<Command> redoStack = new ArrayDeque<>();

    public void perform(Command command) {
        command.execute();
        undoStack.push(command);
        redoStack.clear();                    // a new action makes redo history invalid
    }
    public void undo() {
        Command command = undoStack.poll();
        if (command == null) return;
        command.undo();
        redoStack.push(command);
    }
    public void redo() {
        Command command = redoStack.poll();
        if (command == null) return;
        command.execute();
        undoStack.push(command);
    }
}
interface Command {
  execute(): void;
  undo(): void;
}

class MoveShape implements Command {
  constructor(
    private readonly canvas: Canvas,
    private readonly shapeId: string,
    private readonly dx: number,
    private readonly dy: number,
  ) {}
  execute(): void { this.canvas.move(this.shapeId, this.dx, this.dy); }
  undo(): void { this.canvas.move(this.shapeId, -this.dx, -this.dy); }
}

class History {
  private undoStack: Command[] = [];
  private redoStack: Command[] = [];

  perform(command: Command): void {
    command.execute();
    this.undoStack.push(command);
    this.redoStack = [];                      // a new action makes redo history invalid
  }
  undo(): void {
    const command = this.undoStack.pop();
    if (!command) return;
    command.undo();
    this.redoStack.push(command);
  }
  redo(): void {
    const command = this.redoStack.pop();
    if (!command) return;
    command.execute();
    this.undoStack.push(command);
  }
}
  • Each command is small: MoveShape remembers only the shape and the offset, not a copy of the canvas. 200 moves cost almost no memory.
  • Undo reverses the command (move back by the same offset); redo executes it again.
  • A new action clears redo: after you undo and then draw something new, the undone steps no longer make sense on top of it.
  • Buttons, menus and shortcuts all just call history.perform(MoveShape(...)); they don’t know how moving works.

Predict the output

A text editor starts empty. The user types 'a' (one command), types 'b', presses Undo, types 'c', then presses Redo. What's the text?

Spot the bugUndo restores the wrong color

Tap the line with the bug.

final class SetColor: Command {
    private let shape: Shape
    private let newColor: Color
    private let oldColor: Color

    init(shape: Shape, newColor: Color) {
        self.shape = shape
        self.newColor = newColor
        self.oldColor = shape.color
    }
    func execute() { shape.color = newColor }
    func undo() { shape.color = oldColor }
}
class SetColor(private val shape: Shape, private val newColor: Color) : Command {
    private val oldColor = shape.color

    override fun execute() { shape.color = newColor }
    override fun undo() { shape.color = oldColor }
}
public final class SetColor implements Command {
    private final Shape shape;
    private final Color newColor;
    private final Color oldColor;

    public SetColor(Shape shape, Color newColor) {
        this.shape = shape;
        this.newColor = newColor;
        this.oldColor = shape.getColor();
    }
    public void execute() { shape.setColor(newColor); }
    public void undo() { shape.setColor(oldColor); }
}
class SetColor implements Command {
  private readonly oldColor: Color;

  constructor(private readonly shape: Shape, private readonly newColor: Color) {
    this.oldColor = shape.color;
  }
  execute(): void { this.shape.color = this.newColor; }
  undo(): void { this.shape.color = this.oldColor; }
}

Exerciseeasy

Group commands into one undo step

“Align left” moves five shapes at once. Undo should reverse all five moves in one step, not one at a time. Write a MacroCommand that holds a list of commands.

›

Executing runs the commands in order. Undoing must reverse them in the opposite order, because later commands may depend on earlier ones.

SolutionTry it yourself first
final class MacroCommand: Command {
    private let commands: [Command]
    init(_ commands: [Command]) { self.commands = commands }
    func execute() { commands.forEach { $0.execute() } }
    func undo() { commands.reversed().forEach { $0.undo() } }
}

history.perform(MacroCommand(selected.map { MoveShape(canvas: canvas, shapeId: $0.id, offset: offsetToLeft($0)) }))
class MacroCommand(private val commands: List<Command>) : Command {
    override fun execute() = commands.forEach { it.execute() }
    override fun undo() = commands.asReversed().forEach { it.undo() }
}

history.perform(MacroCommand(selected.map { MoveShape(canvas, it.id, -it.x, 0f) }))
public final class MacroCommand implements Command {
    private final List<Command> commands;
    public MacroCommand(List<Command> commands) { this.commands = List.copyOf(commands); }
    public void execute() { commands.forEach(Command::execute); }
    public void undo() { commands.reversed().forEach(Command::undo); }   // Java 21
}
class MacroCommand implements Command {
  constructor(private readonly commands: Command[]) {}
  execute(): void { this.commands.forEach((c) => c.execute()); }
  undo(): void { [...this.commands].reverse().forEach((c) => c.undo()); }
}

MacroCommand is itself a Command, so the history doesn’t need to know about groups at all. That’s the Composite idea applied to commands.

Undo in real apps

  • Coalescing: typing “hello” shouldn’t take five undos. Merge consecutive commands of the same kind within a time window (UndoManager groups by run loop cycle; editors merge typing until a pause or cursor jump).
  • Inverse vs snapshot: inverse operations are compact but fragile when operations don’t commute; snapshots of the affected object (not the whole document) are simpler for complex edits. Persistent data structures make snapshots cheap.
  • Collaborative editing: with other users editing concurrently, a local inverse can’t be applied blindly; operational transformation or CRDT-based undo transforms the inverse against later remote operations.
  • Bounded history: cap the stack (by count or memory) and drop the oldest entries.
  • Command encapsulates a request: invoker decoupled from receiver; storable, composable (macro), loggable, undoable.
  • Undo via inverse operations (capture overwritten state at execute, every time) or memento snapshots.
  • New command clears redo; macros undo in reverse order; coalesce typing; bound the history.
  • Collaborative undo needs OT/CRDT-aware inverses.

Check this part

  1. What does a command object hold for undo to work?

  2. Why is the redo stack cleared when a new command runs?

  3. When should a command read the value it will overwrite?

A command object wraps one action with everything it needs to run (execute) and to reverse itself (undo). An undo stack holds executed commands; undo pops and reverses one and pushes it onto a redo stack; any new command clears the redo stack.

Part Not started

Queues, offline work and replay

Because a command is an object, it doesn’t have to run right away. An app can queue commands: when the user edits a note offline, the app saves an UpdateNote command, shows the change immediately, and sends the queued commands to the server when the connection returns.

For that to survive the app being closed, a queued command must be data, not code: a small record with a type and its parameters, saved in the local database. A closure like { api.updateNote(note) } can’t be saved to disk.

Commands persisted as data (type + payload + id + timestamp) form an outbox on the client: append on user action, apply optimistically to local state, drain in order with a worker (WorkManager, BGTaskScheduler, a service worker), and delete on acknowledgement. Delivery is at-least-once (the app can die after the server applied a command but before it was removed), so every command carries a client-generated id the server uses for idempotencyIdempotencyAn operation is idempotent if doing it twice has the same effect as doing it once.Open in glossary →. Ordering is usually per entity; conflicts between queued commands and server state need a policy. The same shape on the server is a command bus or message queue; CQRS uses “command” for state-changing requests.

enum NoteCommand: Codable {
    case create(id: UUID, title: String)
    case rename(id: UUID, title: String)
    case delete(id: UUID)
}

struct QueuedCommand: Codable {
    let commandId: UUID                       // for idempotency on the server
    let command: NoteCommand
    let createdAt: Date
}

actor CommandQueue {
    private let store: QueueStore             // persisted, e.g. SwiftData or a file
    private let api: NotesAPI

    init(store: QueueStore, api: NotesAPI) { self.store = store; self.api = api }

    func enqueue(_ command: NoteCommand) throws {
        try store.append(QueuedCommand(commandId: UUID(), command: command, createdAt: .now))
    }

    func drain() async {
        while let next = try? store.first() {
            do {
                try await api.send(next)          // server ignores a commandId it has seen
                try store.removeFirst()
            } catch {
                return                            // offline or failing: try again later, in order
            }
        }
    }
}
@Serializable
sealed interface NoteCommand {
    @Serializable data class Create(val id: String, val title: String) : NoteCommand
    @Serializable data class Rename(val id: String, val title: String) : NoteCommand
    @Serializable data class Delete(val id: String) : NoteCommand
}

@Entity(tableName = "command_queue")
data class QueuedCommand(
    @PrimaryKey val commandId: String = UUID.randomUUID().toString(),   // for idempotency
    val payload: String,                                                 // JSON of NoteCommand
    val createdAt: Long = System.currentTimeMillis(),
)

class CommandQueue(private val dao: CommandDao, private val api: NotesApi, private val json: Json) {
    suspend fun enqueue(command: NoteCommand) =
        dao.insert(QueuedCommand(payload = json.encodeToString(command)))

    suspend fun drain(): Boolean {            // called from a WorkManager worker
        while (true) {
            val next = dao.oldest() ?: return true
            try {
                api.send(next.commandId, json.decodeFromString<NoteCommand>(next.payload))
                dao.delete(next)
            } catch (e: IOException) {
                return false                  // retry later, in order
            }
        }
    }
}
public sealed interface NoteCommand permits CreateNote, RenameNote, DeleteNote {}
public record CreateNote(String id, String title) implements NoteCommand {}
public record RenameNote(String id, String title) implements NoteCommand {}
public record DeleteNote(String id) implements NoteCommand {}

public record QueuedCommand(String commandId, NoteCommand command, Instant createdAt) {}

public final class CommandQueue {
    private final QueueStore store;           // persisted
    private final NotesApi api;

    public CommandQueue(QueueStore store, NotesApi api) { this.store = store; this.api = api; }

    public void enqueue(NoteCommand command) {
        store.append(new QueuedCommand(UUID.randomUUID().toString(), command, Instant.now()));
    }

    public boolean drain() {
        Optional<QueuedCommand> next;
        while ((next = store.oldest()).isPresent()) {
            try {
                api.send(next.get());             // server ignores a commandId it has seen
                store.remove(next.get());
            } catch (UncheckedIOException e) {
                return false;                     // retry later, in order
            }
        }
        return true;
    }
}
type NoteCommand =
  | { type: "create"; id: string; title: string }
  | { type: "rename"; id: string; title: string }
  | { type: "delete"; id: string };

interface QueuedCommand { commandId: string; command: NoteCommand; createdAt: number }

class CommandQueue {
  constructor(private readonly store: QueueStore, private readonly api: NotesApi) {}  // e.g. IndexedDB

  async enqueue(command: NoteCommand): Promise<void> {
    await this.store.append({ commandId: crypto.randomUUID(), command, createdAt: Date.now() });
  }

  async drain(): Promise<boolean> {
    for (let next = await this.store.oldest(); next; next = await this.store.oldest()) {
      try {
        await this.api.send(next);            // server ignores a commandId it has seen
        await this.store.remove(next.commandId);
      } catch {
        return false;                         // offline: retry later, in order
      }
    }
    return true;
  }
}
  • Commands are plain data (an enum or sealed type with parameters), so they can be saved in the database and survive a restart.
  • The queue is sent in order, oldest first: “create note” must reach the server before “rename note”.
  • On failure, stop and try later, instead of skipping ahead and sending commands out of order.
  • commandId lets the server recognize a command it already applied, which matters for the next puzzle.

Production puzzles

Spot the bugTwo orders for one tap

Tap the line with the bug.

func drain() async {
    while let next = try? store.first() {
        do {
            try await api.placeOrder(next.cart)
            try store.removeFirst()
        } catch { return }
    }
}
suspend fun drain() {
    while (true) {
        val next = dao.oldest() ?: return
        try {
            api.placeOrder(next.cart)
            dao.delete(next)
        } catch (e: IOException) { return }
    }
}
public void drain() {
    Optional<QueuedOrder> next;
    while ((next = store.oldest()).isPresent()) {
        try {
            api.placeOrder(next.get().cart());
            store.remove(next.get());
        } catch (UncheckedIOException e) { return; }
    }
}
async drain(): Promise<void> {
  for (let next = await this.store.oldest(); next; next = await this.store.oldest()) {
    try {
      await this.api.placeOrder(next.cart);
      await this.store.remove(next.id);
    } catch { return; }
  }
}

Predict the output

Offline, the user creates a note, renames it, then deletes it. The queue holds three commands. When the network comes back, what should reach the server?

  • Persist commands as data (type, payload, client id, timestamp); apply optimistically; drain in order per entity with a background worker; remove on ack.
  • At-least-once: client-generated id at creation time as the idempotency key; server returns the original result for repeats.
  • Optional compaction of commands per entity; conflict policy against server state.
  • Server side: command bus/queues; CQRS commands as intent-named, validated requests.

Check this part

  1. Why store queued commands as data instead of closures?

  2. Why send queued commands in order?

  3. When must a queued command's idempotency key be generated?

Commands as data can be queued, persisted and sent later: an offline queue stores each user action as a serializable command and replays it when the network is back. Give each command an id so the server can ignore duplicates, and keep them in order.

Key takeaways

  • A command is an action turned into an object: it can run later, be undone, be queued.
  • Undo stack + redo stack; a new action clears redo; macros undo in reverse order.
  • Capture what a command overwrites when it executes, not when it’s created.
  • Offline queues store commands as data, send them in order, and give each an id.
  • Invoker decoupled from receiver; commands are storable, composable and replayable.
  • Inverse operations vs snapshots; coalescing and bounded history.
  • Persisted command queues are client-side outboxes: ordered, at-least-once, idempotent by client id.
  • Command vs Strategy: a command is a request to do something now or later; a strategy is a way of doing it.

Finished this page?Nice work, this page is complete!

Mark it as done to track your progress. It's saved in this browser, no account needed.

Its cards are now in your Bites review ⚡
Esc

Type to search the whole site.