Command Pattern
Command turns a method call into a first-class object you can store, queue, schedule, undo, log, or replay.
Introduction
Command turns a method call into a first-class object you can store, queue, schedule, undo, log, or replay. The invoker triggers commands without knowing what they do; the command knows receiver and operation. Shows up in task queues, CQRS, undo stacks, UI actions, and Kubernetes desired-state declarations.
The story
A document editor needed undo across 40 operations — each had been a direct method call, un-undoable. Command per operation with execute() and undo() made undo/redo a stack push/pop and enabled macros and crash recovery.
The business problem
Direct method calls can't be queued, undone, or audited:
- No undo: operations mutate state with no reversal path.
- No audit trail: compliance can't replay what admin did.
- Invoker coupling: button/scheduler knows every operation type.
- No async queue: long work blocks caller — no retry/replay.
The problem teams faced
Command fits when:
- You need undo/redo/replay.
- Requests must be queued, logged, or retried.
- Same invoker triggers many different operations.
- Operations cross transaction boundaries (outbox, WAL).
Understanding the topic
Intent: Encapsulate a request as an object.
- Command — execute() and often undo().
- ConcreteCommand — binds receiver + parameters.
- Invoker — triggers commands (button, queue, scheduler).
- Receiver — performs actual work.
Internal architecture
Undoable command:
interface Command { execute(): void; undo(): void }class InsertTextCommand implements Command {private prevText: stringconstructor(private doc: Document, private text: string, private pos: number) {}execute() {this.prevText = this.doc.getText(this.pos, this.text.length)this.doc.insert(this.pos, this.text)}undo() { this.doc.replace(this.pos, this.text.length, this.prevText) }}class Editor {private undoStack: Command[] = []run(cmd: Command) { cmd.execute(); this.undoStack.push(cmd) }undo() { const cmd = this.undoStack.pop(); cmd?.undo() }}
Visual explanation
Three diagrams cover command flow, undo stack, and queue:
Informative example
Before / after — direct calls to Command:
// ❌ Direct mutation — no undo, no auditfunction onDeleteRow(table: Table, rowId: string) {table.deleteRow(rowId)}// ✅ Command — undoable + auditableclass DeleteRowCommand implements Command {private deletedRow?: Rowconstructor(private table: Table, private rowId: string) {}execute() {this.deletedRow = this.table.getRow(this.rowId)this.table.deleteRow(this.rowId)}undo() { if (this.deletedRow) this.table.insertRow(this.deletedRow) }}// Invokereditor.run(new DeleteRowCommand(table, rowId))auditLog.persist(command) // serialize intent before execute
Execution workflow
Client builds
ConcreteCommand bound to receiver + args.
Real-world use
Java Runnable/Callable, Swing Action, Sidekiq/Celery/BullMQ jobs, CQRS command buses, Git operations, Kubernetes controllers, transactional outbox messages.
Production case study
Admin audit + undo:
- Scenario: SaaS needed audit trail of every admin action with rollback.
- Decision: Each admin op as Command persisted before execute.
- Outcome: Full compliance audit; ops got "undo last admin action."
Trade-offs
- Pro: undo, queue, audit, replay from one abstraction.
- Pro: invoker decoupled from operations.
- Con: boilerplate per operation class.
- Con: undo complexity for non-reversible side effects (email sent).
Decision framework
- Use when undo/redo required.
- Use when operations queued async with retry.
- Use when audit trail needs intent serialization.
- Avoid for simple synchronous CRUD with no undo/audit need.
Best practices
- Commands carry intent + data — not domain objects with behaviour.
- Compensating commands for irreversible async effects (Saga).
- Idempotent execute when replayed from queue.
Anti-patterns to avoid
- Command that is just a function wrapper with no undo/queue/audit benefit.
- God-command doing unrelated operations — split by intent.
- Undo that doesn't capture sufficient state — corrupts document.
Common mistakes
- Undo after irreversible side effect (email) — need compensating action.
- Command serialization versioning when schema changes.
- Huge command payloads — store reference not full aggregate.
Debugging tips
- Log command type + payload at execute and undo.
- Replay command log in test to reproduce production state.
Optimization strategies
- Batch small commands into macro command for undo granularity control.
- Compress command log for long-running sessions.
Common misconceptions
- Command (intent) ≠ Domain Event (fact). Command: do X. Event: X happened.
- CQRS command bus is Command pattern at architecture scale.
- Runnable is Command — minimal form.
Advanced interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1IntermediateQuestionCommand vs Strategy?+
Answer
Follow-up
2AdvancedQuestionCommand vs Domain Event?+
Answer
Follow-up
3AdvancedQuestionUndo for sent email?+
Answer
Follow-up
4BeginnerQuestionWhere Command in UI?+
Answer
Follow-up
Summary
You can model undo stacks, job queues, and audit trails with Command. Tell the 40-operation document editor story — if you can do that, you own Command.