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"]
endFour 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| Component | Role | Analogy |
|---|---|---|
| Command interface | Contract for all commands: Execute, Undo | Standard instruction sheet |
| Concrete Command | Stores receiver + parameters; knows how to Execute and Undo | A filled-in specific instruction |
| Receiver | The object that actually does the work | The person carrying out the instruction |
| Invoker | Stores and executes commands; knows no details | The 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/sqluses the Command concept throughsql.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.WithCancelenables 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).