Command Pattern #

A text editor needs an undo feature. Every time a user types a character, deletes a word, or formats text, that action must be reversible and repeatable. The problem: how do you store the “trace” of each action so it can be reversed? You cannot just store the string “typed letter A” — you need to store all the information sufficient to reverse that action: at what position, what was there before, what changed. The Command Pattern turns every action into an object — an object that carries all the information needed to execute, undo, and even redo that action. Command objects can be stored in a stack, put in a queue, sent over the network, or scheduled for later. The code that requests execution does not need to know how the action is performed — it only knows there is a command object that can be Execute()d.

What Is the Command Pattern? #

The Command Pattern is a behavioral design pattern that turns a request into a self-contained object holding all the information needed to carry out that request — including the receiver that will perform it, the method to call, and the required parameters. A request in object form can be stored, queued, sent, logged, or reversed.

The “request → object” transformation is the key. It unlocks four capabilities that are impossible with plain function calls:

  • Deferred execution — store the command, execute later
  • Undo/Redo — every command knows how to reverse itself
  • Queue and scheduling — commands can be queued for sequential or scheduled execution
  • Macro commands — combine several commands into one larger command
flowchart LR
    subgraph "Without Command"
        C1[Client] -->|"editor.insertChar('A', pos)"| R1[TextEditor]
        C1 -->|"editor.deleteChar(pos)"| R1
        C1 -->|"editor.formatBold(range)"| R1
        note1["No trace,\\nno undo"]
    end

    subgraph "With Command"
        C2[Client] -->|"Execute()"| CMD[InsertCharCommand\\n- char: 'A'\\n- pos: 5\\n- prevState: ...]
        CMD -->|"receiver.Insert()"| R2[TextEditor]
        CMD -->|"stored in history"| H[UndoStack]
        note2["Can undo, redo,\\ncan replay"]
    end

Four Components of the Command Pattern #

The Command Pattern involves four components with different but complementary roles.

classDiagram
    class Command {
        <<interface>>
        +Execute() error
        +Undo() error
        +Description() string
    }

    class TextEditor {
        -content []rune
        -cursor int
        +Insert(pos int, char rune)
        +Delete(pos int) rune
        +GetContent() string
    }

    class InsertCommand {
        -editor TextEditor
        -char rune
        -position int
        +Execute() error
        +Undo() error
        +Description() string
    }

    class DeleteCommand {
        -editor TextEditor
        -position int
        -deletedChar rune
        +Execute() error
        +Undo() error
        +Description() string
    }

    class CommandHistory {
        -undoStack []Command
        -redoStack []Command
        +Execute(cmd Command) error
        +Undo() error
        +Redo() error
    }

    Command <|.. InsertCommand
    Command <|.. DeleteCommand
    InsertCommand --> TextEditor : receiver
    DeleteCommand --> TextEditor : receiver
    CommandHistory o-- Command : manages history
ComponentRoleAnalogy
Command interfaceContract for all commands: Execute, UndoStandard instruction sheet
Concrete CommandStores receiver + parameters; knows how to Execute and UndoA filled-in specific instruction
ReceiverThe object that actually does the workThe person carrying out the instruction
InvokerStores and executes commands; knows no detailsThe manager passing along instructions

Full Implementation: Text Editor with Undo/Redo #

Receiver: TextEditor #

package editor

import (
    "fmt"
    "strings"
)

// TextEditor is the Receiver — the component that actually executes text operations.
// There is no undo logic here; undo is the Command's responsibility.
type TextEditor struct {
    content []rune
    cursor  int
}

func NewTextEditor(initialContent string) *TextEditor {
    return &TextEditor{
        content: []rune(initialContent),
        cursor:  len([]rune(initialContent)),
    }
}

// Insert inserts a rune at the given position.
func (e *TextEditor) Insert(pos int, char rune) error {
    if pos < 0 || pos > len(e.content) {
        return fmt.Errorf("invalid position %d (content length: %d)", pos, len(e.content))
    }
    e.content = append(e.content[:pos], append([]rune{char}, e.content[pos:]...)...)
    if e.cursor >= pos {
        e.cursor++
    }
    return nil
}

// Delete removes the rune at the given position and returns the deleted rune.
func (e *TextEditor) Delete(pos int) (rune, error) {
    if pos < 0 || pos >= len(e.content) {
        return 0, fmt.Errorf("invalid position %d (content length: %d)", pos, len(e.content))
    }
    deleted := e.content[pos]
    e.content = append(e.content[:pos], e.content[pos+1:]...)
    if e.cursor > pos {
        e.cursor--
    }
    return deleted, nil
}

// Replace replaces the text in a given range.
func (e *TextEditor) Replace(start, end int, newText string) (string, error) {
    if start < 0 || end > len(e.content) || start > end {
        return "", fmt.Errorf("invalid range [%d, %d]", start, end)
    }
    oldText := string(e.content[start:end])
    newRunes := []rune(newText)
    e.content = append(e.content[:start], append(newRunes, e.content[end:]...)...)
    return oldText, nil
}

// GetContent returns the current document content.
func (e *TextEditor) GetContent() string { return string(e.content) }

// GetCursor returns the current cursor position.
func (e *TextEditor) GetCursor() int { return e.cursor }

// SetCursor moves the cursor to a specific position.
func (e *TextEditor) SetCursor(pos int) { e.cursor = pos }

Command Interface #

package editor

// Command is the interface for every editor operation.
// Each Command must be able to Execute and Undo.
type Command interface {
    // Execute runs the operation.
    Execute() error

    // Undo reverses an already-executed operation.
    Undo() error

    // Description returns a short description for logging and UI.
    Description() string
}

Concrete Commands #

package editor

import "fmt"

// InsertCommand inserts a single character into the document.
type InsertCommand struct {
    editor   *TextEditor
    char     rune
    position int
    executed bool
}

func NewInsertCommand(editor *TextEditor, char rune, position int) Command {
    return &InsertCommand{editor: editor, char: char, position: position}
}

func (c *InsertCommand) Execute() error {
    if err := c.editor.Insert(c.position, c.char); err != nil {
        return fmt.Errorf("insert failed: %w", err)
    }
    c.executed = true
    return nil
}

func (c *InsertCommand) Undo() error {
    if !c.executed {
        return fmt.Errorf("cannot undo: command has not been executed")
    }
    if _, err := c.editor.Delete(c.position); err != nil {
        return fmt.Errorf("undo insert failed: %w", err)
    }
    c.executed = false
    return nil
}

func (c *InsertCommand) Description() string {
    return fmt.Sprintf("Insert '%c' at position %d", c.char, c.position)
}


// DeleteCommand removes a single character from the document.
type DeleteCommand struct {
    editor      *TextEditor
    position    int
    deletedChar rune  // stored during Execute for Undo purposes
    executed    bool
}

func NewDeleteCommand(editor *TextEditor, position int) Command {
    return &DeleteCommand{editor: editor, position: position}
}

func (c *DeleteCommand) Execute() error {
    deleted, err := c.editor.Delete(c.position)
    if err != nil {
        return fmt.Errorf("delete failed: %w", err)
    }
    c.deletedChar = deleted
    c.executed = true
    return nil
}

func (c *DeleteCommand) Undo() error {
    if !c.executed {
        return fmt.Errorf("cannot undo: command has not been executed")
    }
    if err := c.editor.Insert(c.position, c.deletedChar); err != nil {
        return fmt.Errorf("undo delete failed: %w", err)
    }
    c.executed = false
    return nil
}

func (c *DeleteCommand) Description() string {
    if c.executed {
        return fmt.Sprintf("Delete '%c' at position %d", c.deletedChar, c.position)
    }
    return fmt.Sprintf("Delete at position %d", c.position)
}


// ReplaceCommand replaces text within a range.
type ReplaceCommand struct {
    editor   *TextEditor
    start    int
    end      int
    newText  string
    oldText  string // stored during Execute for Undo purposes
    executed bool
}

func NewReplaceCommand(editor *TextEditor, start, end int, newText string) Command {
    return &ReplaceCommand{editor: editor, start: start, end: end, newText: newText}
}

func (c *ReplaceCommand) Execute() error {
    oldText, err := c.editor.Replace(c.start, c.end, c.newText)
    if err != nil {
        return fmt.Errorf("replace failed: %w", err)
    }
    c.oldText = oldText
    c.executed = true
    return nil
}

func (c *ReplaceCommand) Undo() error {
    if !c.executed {
        return fmt.Errorf("cannot undo: command has not been executed")
    }
    // Calculate the new end position after newText was inserted
    newEnd := c.start + len([]rune(c.newText))
    if _, err := c.editor.Replace(c.start, newEnd, c.oldText); err != nil {
        return fmt.Errorf("undo replace failed: %w", err)
    }
    c.executed = false
    return nil
}

func (c *ReplaceCommand) Description() string {
    return fmt.Sprintf("Replace [%d:%d] with %q", c.start, c.end, c.newText)
}

Macro Command: Combining Several Commands #

// MacroCommand combines several commands into one unit that can be Executed and Undone.
// Useful for operations consisting of several steps that should be treated as atomic.
type MacroCommand struct {
    commands    []Command
    description string
    executed    int // number of commands already successfully executed
}

func NewMacroCommand(description string, commands ...Command) Command {
    return &MacroCommand{
        commands:    commands,
        description: description,
    }
}

func (m *MacroCommand) Execute() error {
    for i, cmd := range m.commands {
        if err := cmd.Execute(); err != nil {
            // Roll back everything that already succeeded
            for j := i - 1; j >= 0; j-- {
                _ = m.commands[j].Undo()
            }
            return fmt.Errorf("macro failed at step %d (%s): %w", i+1, cmd.Description(), err)
        }
        m.executed++
    }
    return nil
}

func (m *MacroCommand) Undo() error {
    // Undo in reverse order
    for i := m.executed - 1; i >= 0; i-- {
        if err := m.commands[i].Undo(); err != nil {
            return fmt.Errorf("macro undo failed at step %d: %w", i+1, err)
        }
    }
    m.executed = 0
    return nil
}

func (m *MacroCommand) Description() string { return m.description }

Invoker: CommandHistory with Undo/Redo #

// CommandHistory is the Invoker — it stores command history and orchestrates undo/redo.
type CommandHistory struct {
    undoStack []Command
    redoStack []Command
    maxSize   int
}

func NewCommandHistory(maxSize int) *CommandHistory {
    return &CommandHistory{
        undoStack: make([]Command, 0, maxSize),
        redoStack: make([]Command, 0, maxSize),
        maxSize:   maxSize,
    }
}

// Execute runs a command and stores it in the undo stack.
// Every new Execute clears the redo stack.
func (h *CommandHistory) Execute(cmd Command) error {
    if err := cmd.Execute(); err != nil {
        return err
    }

    // Add to the undo stack
    if len(h.undoStack) >= h.maxSize {
        // Remove the oldest command if the limit is exceeded
        h.undoStack = h.undoStack[1:]
    }
    h.undoStack = append(h.undoStack, cmd)

    // Clear the redo stack — a new action invalidates all "available redos"
    h.redoStack = h.redoStack[:0]

    return nil
}

// Undo reverses the last executed command.
func (h *CommandHistory) Undo() error {
    if len(h.undoStack) == 0 {
        return fmt.Errorf("nothing to undo")
    }

    // Pop from the undo stack
    lastIdx := len(h.undoStack) - 1
    cmd := h.undoStack[lastIdx]
    h.undoStack = h.undoStack[:lastIdx]

    if err := cmd.Undo(); err != nil {
        // Put it back on the undo stack on failure
        h.undoStack = append(h.undoStack, cmd)
        return fmt.Errorf("undo failed: %w", err)
    }

    // Move it to the redo stack
    h.redoStack = append(h.redoStack, cmd)
    return nil
}

// Redo repeats the last undone command.
func (h *CommandHistory) Redo() error {
    if len(h.redoStack) == 0 {
        return fmt.Errorf("nothing to redo")
    }

    // Pop from the redo stack
    lastIdx := len(h.redoStack) - 1
    cmd := h.redoStack[lastIdx]
    h.redoStack = h.redoStack[:lastIdx]

    if err := cmd.Execute(); err != nil {
        h.redoStack = append(h.redoStack, cmd)
        return fmt.Errorf("redo failed: %w", err)
    }

    h.undoStack = append(h.undoStack, cmd)
    return nil
}

// CanUndo checks whether there is a command that can be undone.
func (h *CommandHistory) CanUndo() bool { return len(h.undoStack) > 0 }

// CanRedo checks whether there is a command that can be redone.
func (h *CommandHistory) CanRedo() bool { return len(h.redoStack) > 0 }

// UndoCount returns the number of commands that can be undone.
func (h *CommandHistory) UndoCount() int { return len(h.undoStack) }

// History returns descriptions of all commands in the undo stack (for UI).
func (h *CommandHistory) History() []string {
    result := make([]string, len(h.undoStack))
    for i, cmd := range h.undoStack {
        result[i] = cmd.Description()
    }
    return result
}

Usage Demonstration #

func main() {
    editor := editor.NewTextEditor("Hello")
    history := editor.NewCommandHistory(50)

    fmt.Printf("Initial: %q\n", editor.GetContent())

    // Execute several commands
    _ = history.Execute(editor.NewInsertCommand(editor, ' ', 5))
    _ = history.Execute(editor.NewInsertCommand(editor, 'W', 6))
    _ = history.Execute(editor.NewInsertCommand(editor, 'o', 7))
    _ = history.Execute(editor.NewInsertCommand(editor, 'r', 8))
    _ = history.Execute(editor.NewInsertCommand(editor, 'l', 9))
    _ = history.Execute(editor.NewInsertCommand(editor, 'd', 10))

    fmt.Printf("After typing: %q\n", editor.GetContent()) // "Hello World"

    // Undo 3 times
    _ = history.Undo()
    _ = history.Undo()
    _ = history.Undo()
    fmt.Printf("After 3 undos: %q\n", editor.GetContent()) // "Hello Wor"

    // Redo once
    _ = history.Redo()
    fmt.Printf("After 1 redo: %q\n", editor.GetContent()) // "Hello Worl"

    // Macro command: replace "Hello" with "Hi"
    findReplace := editor.NewMacroCommand("Replace Hello with Hi",
        editor.NewDeleteCommand(editor, 4), // delete 'o'
        editor.NewDeleteCommand(editor, 3), // delete 'l'
        editor.NewDeleteCommand(editor, 2), // delete 'l'
        editor.NewDeleteCommand(editor, 1), // delete 'e'
        editor.NewInsertCommand(editor, 'i', 1), // insert 'i'
    )
    _ = history.Execute(findReplace)
    fmt.Printf("After macro: %q\n", editor.GetContent()) // "Hi Worl"

    // Undo the entire macro at once
    _ = history.Undo()
    fmt.Printf("After undo macro: %q\n", editor.GetContent()) // "Hello Worl"

    fmt.Printf("History (%d commands):\n", history.UndoCount())
    for i, h := range history.History() {
        fmt.Printf("  %d. %s\n", i+1, h)
    }
}

Second Case Study: Job Queue System #

The Command Pattern is the foundation of job queues — every job is a command that can be stored, queued, and executed by a worker pool.

package jobqueue

import (
    "context"
    "fmt"
    "log/slog"
    "sync"
    "time"
)

// Job is the Command interface for the job queue system.
type Job interface {
    Execute(ctx context.Context) error
    JobID() string
    JobType() string
    MaxRetries() int
}

// JobQueue is the Invoker that manages the queue and job execution.
type JobQueue struct {
    queue   chan Job
    workers int
    logger  *slog.Logger
    wg      sync.WaitGroup
    ctx     context.Context
    cancel  context.CancelFunc
}

func NewJobQueue(workers, bufferSize int, logger *slog.Logger) *JobQueue {
    ctx, cancel := context.WithCancel(context.Background())
    return &JobQueue{
        queue:   make(chan Job, bufferSize),
        workers: workers,
        logger:  logger,
        ctx:     ctx,
        cancel:  cancel,
    }
}

// Start launches the worker pool.
func (q *JobQueue) Start() {
    for i := 0; i < q.workers; i++ {
        q.wg.Add(1)
        go q.worker(i)
    }
}

// Enqueue puts a job into the queue.
func (q *JobQueue) Enqueue(job Job) error {
    select {
    case q.queue <- job:
        q.logger.Info("job enqueued", "job_id", job.JobID(), "type", job.JobType())
        return nil
    case <-q.ctx.Done():
        return fmt.Errorf("queue is shutting down")
    default:
        return fmt.Errorf("queue is full")
    }
}

// Stop shuts the queue down gracefully.
func (q *JobQueue) Stop() {
    q.cancel()
    close(q.queue)
    q.wg.Wait()
}

func (q *JobQueue) worker(id int) {
    defer q.wg.Done()
    for job := range q.queue {
        q.executeWithRetry(job)
    }
}

func (q *JobQueue) executeWithRetry(job Job) {
    var lastErr error
    for attempt := 0; attempt <= job.MaxRetries(); attempt++ {
        if attempt > 0 {
            time.Sleep(time.Duration(attempt) * time.Second)
            q.logger.Info("retrying job", "job_id", job.JobID(), "attempt", attempt+1)
        }

        if err := job.Execute(q.ctx); err != nil {
            lastErr = err
            q.logger.Warn("job failed", "job_id", job.JobID(), "attempt", attempt+1, "error", err)
            continue
        }

        q.logger.Info("job completed", "job_id", job.JobID())
        return
    }

    q.logger.Error("job exhausted retries",
        "job_id", job.JobID(),
        "max_retries", job.MaxRetries(),
        "last_error", lastErr,
    )
}

Concrete Jobs — every job type is a concrete command:

// SendEmailJob is a concrete Command for sending emails.
type SendEmailJob struct {
    id         string
    recipient  string
    subject    string
    body       string
    emailSvc   EmailService
}

func NewSendEmailJob(id, recipient, subject, body string, svc EmailService) Job {
    return &SendEmailJob{
        id: id, recipient: recipient,
        subject: subject, body: body, emailSvc: svc,
    }
}

func (j *SendEmailJob) Execute(ctx context.Context) error {
    return j.emailSvc.Send(ctx, j.recipient, j.subject, j.body)
}

func (j *SendEmailJob) JobID() string    { return j.id }
func (j *SendEmailJob) JobType() string  { return "send_email" }
func (j *SendEmailJob) MaxRetries() int  { return 3 }


// GenerateReportJob is a concrete Command for generating reports.
type GenerateReportJob struct {
    id        string
    reportType string
    period    string
    outputPath string
    reportSvc  ReportService
}

func NewGenerateReportJob(id, reportType, period, outputPath string, svc ReportService) Job {
    return &GenerateReportJob{
        id: id, reportType: reportType,
        period: period, outputPath: outputPath, reportSvc: svc,
    }
}

func (j *GenerateReportJob) Execute(ctx context.Context) error {
    return j.reportSvc.Generate(ctx, j.reportType, j.period, j.outputPath)
}

func (j *GenerateReportJob) JobID() string    { return j.id }
func (j *GenerateReportJob) JobType() string  { return "generate_report" }
func (j *GenerateReportJob) MaxRetries() int  { return 1 }

// Usage
func setupJobQueue() {
    queue := jobqueue.NewJobQueue(5, 100, slog.Default())
    queue.Start()

    // Enqueue various job types
    _ = queue.Enqueue(NewSendEmailJob("email-001", "[email protected]",
        "Order Confirmed", "Your order has been confirmed!", emailSvc))
    _ = queue.Enqueue(NewGenerateReportJob("report-001",
        "financial", "2024-Q1", "/reports/q1.pdf", reportSvc))
    _ = queue.Enqueue(NewSendEmailJob("email-002", "[email protected]",
        "Daily Summary", "Here is your daily summary...", emailSvc))

    // The queue runs all jobs concurrently with retry
    time.Sleep(10 * time.Second)
    queue.Stop()
}

Functional Command: The Go Idiom #

For simpler cases without undo needs, Go allows a very concise Command implementation using function types.

// SimpleCommand is a function type — more concise than a struct for commands without Undo
type SimpleCommand func(ctx context.Context) error

// CommandQueue executes SimpleCommands sequentially.
type CommandQueue struct {
    commands []SimpleCommand
}

func (q *CommandQueue) Add(cmd SimpleCommand) {
    q.commands = append(q.commands, cmd)
}

func (q *CommandQueue) ExecuteAll(ctx context.Context) error {
    for i, cmd := range q.commands {
        if err := cmd(ctx); err != nil {
            return fmt.Errorf("command %d failed: %w", i+1, err)
        }
    }
    return nil
}

// Usage — commands as closures
queue := &CommandQueue{}

queue.Add(func(ctx context.Context) error {
    return userSvc.DeductBalance(ctx, userID, amount)
})

queue.Add(func(ctx context.Context) error {
    return inventorySvc.ReserveStock(ctx, productID, qty)
})

queue.Add(func(ctx context.Context) error {
    return notifSvc.SendConfirmation(ctx, userEmail)
})

if err := queue.ExecuteAll(ctx); err != nil {
    log.Printf("pipeline failed: %v", err)
}

The Correct Undo/Redo Flow #

sequenceDiagram
    participant U as User
    participant H as CommandHistory
    participant C as Command
    participant E as TextEditor

    U->>H: Execute(InsertCommand 'A')
    H->>C: Execute()
    C->>E: Insert('A', pos)
    H->>H: push to undoStack
    H->>H: clear redoStack

    U->>H: Execute(InsertCommand 'B')
    H->>C: Execute()
    C->>E: Insert('B', pos)
    H->>H: push to undoStack

    U->>H: Undo()
    H->>H: pop InsertCommand 'B' from undoStack
    H->>C: Undo()
    C->>E: Delete(pos of 'B')
    H->>H: push to redoStack

    U->>H: Redo()
    H->>H: pop InsertCommand 'B' from redoStack
    H->>C: Execute()
    C->>E: Insert('B', pos)
    H->>H: push to undoStack

    U->>H: Execute(NewCommand)
    H->>H: push to undoStack
    H->>H: clear redoStack ← the redo stack is emptied!

Testing the Command Pattern #

func TestInsertCommand_ExecuteAndUndo(t *testing.T) {
    ed := NewTextEditor("Hello")
    cmd := NewInsertCommand(ed, ' ', 5)

    if err := cmd.Execute(); err != nil {
        t.Fatalf("Execute failed: %v", err)
    }
    if ed.GetContent() != "Hello " {
        t.Errorf("after execute: expected 'Hello ', got %q", ed.GetContent())
    }

    if err := cmd.Undo(); err != nil {
        t.Fatalf("Undo failed: %v", err)
    }
    if ed.GetContent() != "Hello" {
        t.Errorf("after undo: expected 'Hello', got %q", ed.GetContent())
    }
}

func TestDeleteCommand_StoresDeletedChar(t *testing.T) {
    ed := NewTextEditor("Hello")
    cmd := NewDeleteCommand(ed, 4) // delete 'o'

    if err := cmd.Execute(); err != nil {
        t.Fatalf("Execute failed: %v", err)
    }
    if ed.GetContent() != "Hell" {
        t.Errorf("after delete: expected 'Hell', got %q", ed.GetContent())
    }

    if err := cmd.Undo(); err != nil {
        t.Fatalf("Undo failed: %v", err)
    }
    if ed.GetContent() != "Hello" {
        t.Errorf("after undo delete: expected 'Hello', got %q", ed.GetContent())
    }
}

func TestCommandHistory_UndoRedo(t *testing.T) {
    ed := NewTextEditor("")
    history := NewCommandHistory(10)

    _ = history.Execute(NewInsertCommand(ed, 'A', 0))
    _ = history.Execute(NewInsertCommand(ed, 'B', 1))
    _ = history.Execute(NewInsertCommand(ed, 'C', 2))

    if ed.GetContent() != "ABC" {
        t.Errorf("expected 'ABC', got %q", ed.GetContent())
    }

    _ = history.Undo()
    if ed.GetContent() != "AB" {
        t.Errorf("after undo: expected 'AB', got %q", ed.GetContent())
    }

    _ = history.Undo()
    if ed.GetContent() != "A" {
        t.Errorf("after 2 undos: expected 'A', got %q", ed.GetContent())
    }

    _ = history.Redo()
    if ed.GetContent() != "AB" {
        t.Errorf("after redo: expected 'AB', got %q", ed.GetContent())
    }
}

func TestCommandHistory_NewExecuteClearsRedoStack(t *testing.T) {
    ed := NewTextEditor("AB")
    history := NewCommandHistory(10)

    _ = history.Execute(NewInsertCommand(ed, 'C', 2))
    _ = history.Undo()

    if !history.CanRedo() {
        t.Error("expected redo available after undo")
    }

    // Execute a new command — the redo stack must be cleared
    _ = history.Execute(NewInsertCommand(ed, 'D', 2))
    if history.CanRedo() {
        t.Error("expected redo stack cleared after new execute")
    }
}

func TestMacroCommand_RollbackOnFailure(t *testing.T) {
    ed := NewTextEditor("Hello")
    history := NewCommandHistory(10)

    // A macro containing a command that will fail (invalid position)
    macro := NewMacroCommand("Bad Macro",
        NewInsertCommand(ed, 'X', 5),
        NewInsertCommand(ed, 'Y', 999), // invalid position — will fail
    )

    err := history.Execute(macro)
    if err == nil {
        t.Error("expected error from macro with invalid command")
    }

    // Rollback must happen — the content must return to "Hello"
    if ed.GetContent() != "Hello" {
        t.Errorf("expected rollback to 'Hello', got %q", ed.GetContent())
    }
}

When to Use and When Not to #

USE Command if:
  ✓ You need undo/redo — this is the Command Pattern's main use case
  ✓ You need to store an operation history for audit or replay
  ✓ You need to run operations asynchronously or on a schedule
  ✓ You want to combine several operations into one unit (macro)
  ✓ You are building a job queue or task scheduler system

AVOID Command if:
  ✗ There is no need for undo, queue, or scheduling — a plain function is enough
  ✗ The command carries no state at all — consider the Strategy Pattern
  ✗ The struct overhead is too large for very simple operations
  ✗ All commands need to know each other — consider the Mediator

Command in Popular Frameworks

database/sql uses the Command concept through sql.Tx — every query within a transaction is a command that can be rolled back. HTTP request handlers can be seen as Commands in a web framework context. context.WithCancel enables cancelling a running command. Understanding the Command Pattern helps you read and understand these patterns more deeply.


Command Review Checklist #

DESIGN:
  □ Each Command is responsible for exactly one clear action
  □ Commands store all the state needed for Execute and Undo
  □ The Invoker knows no command implementation details — only calls Execute/Undo
  □ The Receiver (the object doing the work) is separate from the Command

UNDO/REDO:
  □ Execute stores the state needed for Undo (e.g., the deleted character)
  □ Undo restores the state exactly to its pre-Execute condition
  □ Undo is not called on a command that has not been Executed
  □ The redo stack is cleared whenever a new Execute happens

MACRO COMMAND:
  □ The macro rolls back all already-succeeded commands if one fails
  □ Rollback happens in the reverse order of execution
  □ The macro has a clear description for logging

TESTING:
  □ Execute and Undo are tested as a pair
  □ The state after Undo is exactly the same as the state before Execute
  □ Macro rollback is tested (one command fails → all are rolled back)
  □ CommandHistory: undo stack and redo stack behaviors are tested

Summary #

  • Command turns a request into an object — not just calling a function; the request becomes an entity that can be stored, queued, sent, and reversed.
  • Four components: the Command interface, Concrete Command (stores receiver + state), Receiver (does the work), and Invoker (manages and executes commands).
  • Undo is the main strength — every Command stores enough state to reverse its action; the Invoker manages the undo/redo stacks.
  • The redo stack must be cleared whenever a new Execute is called — this is an often-missed undo/redo rule.
  • Macro Commands combine several commands into one atomic unit; if one step fails, all previous steps are rolled back.
  • A Job Queue is the Command Pattern — every job is a Command queued by the Invoker (the queue) and executed by a worker (the receiver); retry logic lives in the Invoker.
  • Functional Commands for simple cases — a function type as Command is far more concise for scenarios without Undo; use a struct only when state and Undo are needed.
  • Distinguish it from Strategy: Strategy defines how something is done (swappable algorithms); Command defines what is done (a request that can be stored and reversed).

← Previous: Observer   Next: Chain of Responsibility →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact