Hermes Wiki

Command Pattern

Concept

Command turns a request — an action plus everything it needs to execute (its receiver, its arguments) — into a standalone object with a uniform interface, typically a single execute() method. The code that invokes the action (a button, a scheduler, an API endpoint) holds a reference to a command object and calls execute() without knowing or caring what concrete action it triggers or which object ultimately performs it. Because the request is now an object rather than a direct function call, it can be passed around, stored in a list, serialized, logged, delayed, retried, or reversed — none of which is possible when "do the thing" is just a synchronous method call baked into the caller.

The pattern shows up constantly in infrastructure that most engineers use without naming it as Command: a background job queue (Sidekiq, Celery, BullMQ) is a collection of serialized command objects waiting to be executed by a worker; a text editor's undo stack is a list of executed commands, each capable of reversing itself; GUI toolkits bind buttons and menu items to command objects rather than hardcoded handlers so the same action can be triggered from a button, a keyboard shortcut, and a menu item without duplicating logic.

Tradeoffs

Approach Benefit Cost
Command (action as an object) Actions can be queued, logged, retried, delayed, or undone; invoker and receiver are fully decoupled — the invoker never imports the receiver's class More types in the codebase — one class (or closure/struct) per distinct action; overkill when an action never needs to be queued, logged, or undone
Direct method call Simplest possible code, easy to trace — call stack shows exactly what ran and when No way to queue, delay, retry, or undo without bolting on extra machinery after the fact; caller is coupled to the receiver's concrete interface
Callback / function reference Language-native in most modern runtimes (closures capture their own context), less ceremony than a full command class Loses the ability to introspect the action (a closure can't easily be logged, serialized, or compared for equality) the way a well-named command object can; undo requires the closure itself to carry reversal logic, which gets unwieldy fast

The realistic decision in a modern codebase using a language with first-class closures usually isn't "Command class vs. bare method call" — it's "Command class vs. closure/lambda." A closure gets you the decoupling (the invoker holds a Function, not a reference to the receiver's class) but not the extra structure a real Command object gives you for free: named, inspectable, serializable requests that survive being written to a queue and picked up by a different process entirely. Once execution needs to cross a process or a machine boundary — which any background job system does — Command's "action as a plain-data-plus-behavior object" shape wins over a closure, because a closure can't be serialized and shipped to another worker.

When to use / when not to

  • Use when actions need to be queued, delayed, retried, logged, or undone rather than executed synchronously and forgotten — background job systems, undo/redo stacks, transactional multi-step operations where a later step might need to compensate an earlier one.
  • Use to decouple an invoker (a UI button, an API route) from concrete receiver classes, especially when the same action needs to be triggered from multiple places without duplicating the call logic.
  • Don't use it for a simple, synchronous, always-immediate action with no need for history, retry, or delay — a direct function call is clearer and has less ceremony than a one-method class that will only ever be constructed and immediately invoked.
  • Undo support specifically is only worth the extra undo() method and the state it requires when the domain genuinely needs reversibility (editors, transactional workflows) — don't add it speculatively to every command.

Common pitfall

Modeling a background job as a bare function reference or a loosely-typed dict of arguments instead of an explicit command object, which works fine until the job needs to be retried, logged with structured context, or replayed after a schema change — at which point every call site that enqueues that job needs to be found and updated, because there was never a single class encapsulating "what this job needs to run." An explicit command class (or equivalent typed payload) with its own fields is what makes a job queue's contents inspectable, versionable, and safely serializable across a deploy.

Engineering Lens

The pattern's payoff is visible the first time a production incident requires answering "what jobs were in flight when this happened, and can we safely retry them." A codebase where every async action is an explicit, named command object can answer that from the queue's contents directly; a codebase where actions are anonymous closures or untyped payloads usually can't, and ends up needing extra tracing infrastructure to reconstruct the same information after the fact. The general principle — turning an implicit action into an explicit, inspectable object — is the same instinct behind structured logging over string concatenation: making intent a first-class, queryable thing rather than something you have to infer from a stack trace.

  • Chain of Responsibility Pattern — both decouple a sender from execution details, but Command wraps a single action as an object while Chain of Responsibility routes a request through candidate handlers

Sources

Hermes Wiki