Intent
Turn a request into a stand-alone object that carries everything it needs to execute itself — so the system can queue it, log it, schedule it, retry it on failure, or roll it back later.
The Problem
Imagine your app needs to send invoices, resize uploaded images, and sync new customers to a CRM. Each one is a slow operation, so you want to run them in the background. The first cut looks like this:
public function handleSignup(User $user): void
{
$this->mailer->sendWelcome($user);
$this->crm->sync($user);
$this->images->resizeAvatar($user->avatarPath);
}
It's a synchronous chain. If the CRM is down for three minutes, the signup hangs for three minutes. If the image resize fails, you've lost the work. There's no retry, no log of what was attempted, no way to add a fourth step without editing the controller.
The fix usually starts as "let's queue it" and ends with a homegrown background runner — a tangle of method names, payload arrays, and if ($job_type === 'send_invoice') switches in the worker. You're now writing a job system from scratch, badly.
The Solution
Command says: turn each request into an object with one method — execute(). The Invoker doesn't know what's inside the command; it just calls execute() and moves on. The command itself carries the data and the logic to do its job.
interface Command
{
public function execute(): void;
}
final class SendInvoiceCommand implements Command
{
public function __construct(private int $orderId) {}
public function execute(): void { /* ... */ }
}
Now anything that can hold an object can hold a command — a queue, a log, a retry list, a transaction history. The worker becomes a three-line loop. Adding a fourth job is a new class.
Real-World Analogy
A restaurant order ticket. You speak your order to the waiter — that's an in-memory request, fragile, gone the moment they walk away. The waiter writes it on a slip and drops it in the kitchen queue. Now the order is a physical thing: it can be queued behind other tickets, picked up by whichever chef is free, lost (and recovered from the slip), retried if the dish fails, and held up by the manager as evidence later that night.
That slip is a Command. Same content as the spoken order — but now it has a life of its own.
Structure
Four roles you'll see in every Command implementation:
- Command — the interface every request agrees on. Usually one method:
execute(). - Concrete Command — one class per kind of request:
SendInvoiceCommand,ResizeAvatarCommand. Holds the data needed and knows which Receiver to call. - Invoker — what triggers the command. A queue worker, a button click handler, a scheduler. Doesn't know what's inside; just calls
execute(). - Receiver — the service that does the actual work:
Mailer,ImageProcessor. The Concrete Command delegates to it.
The point of the four-way split: the Invoker is decoupled from the Receivers entirely. You can wire any command to any invoker without either side knowing about the other.
Code Examples
Here's a small job-queue setup in five languages. Notice how the worker (run()) is identical regardless of which command it's running.
interface Command {
execute(): Promise<void>;
}
class SendInvoiceCommand implements Command {
constructor(private orderId: number, private mailer: Mailer) {}
async execute(): Promise<void> {
await this.mailer.sendInvoice(this.orderId);
}
}
class ResizeAvatarCommand implements Command {
constructor(private path: string, private images: ImageProcessor) {}
async execute(): Promise<void> {
await this.images.resize(this.path, { w: 200, h: 200 });
}
}
export class JobWorker {
constructor(private queue: Command[]) {}
async run(): Promise<void> {
while (this.queue.length) {
const cmd = this.queue.shift()!;
await cmd.execute();
}
}
}
from abc import ABC, abstractmethod
class Command(ABC):
@abstractmethod
def execute(self): ...
class SendInvoiceCommand(Command):
def __init__(self, order_id, mailer):
self.order_id, self.mailer = order_id, mailer
def execute(self):
self.mailer.send_invoice(self.order_id)
class ResizeAvatarCommand(Command):
def __init__(self, path, images):
self.path, self.images = path, images
def execute(self):
self.images.resize(self.path, w=200, h=200)
class JobWorker:
def __init__(self, queue):
self._queue = queue
def run(self):
while self._queue:
self._queue.pop(0).execute()
public interface Command {
void execute();
}
public final class SendInvoiceCommand implements Command {
private final long orderId;
private final Mailer mailer;
public SendInvoiceCommand(long orderId, Mailer mailer) {
this.orderId = orderId;
this.mailer = mailer;
}
@Override public void execute() {
mailer.sendInvoice(orderId);
}
}
public final class JobWorker {
private final Queue<Command> queue;
public JobWorker(Queue<Command> queue) {
this.queue = queue;
}
public void run() {
Command cmd;
while ((cmd = queue.poll()) != null) {
cmd.execute();
}
}
}
<?php
namespace App\Jobs;
interface Command
{
public function execute(): void;
}
final class SendInvoiceCommand implements Command
{
public function __construct(private int $orderId, private Mailer $mailer) {}
public function execute(): void
{
$this->mailer->sendInvoice($this->orderId);
}
}
final class ResizeAvatarCommand implements Command
{
public function __construct(private string $path, private ImageProcessor $images) {}
public function execute(): void
{
$this->images->resize($this->path, 200, 200);
}
}
final class JobWorker
{
/** @param Command[] $queue */
public function __construct(private array $queue) {}
public function run(): void
{
while ($cmd = array_shift($this->queue)) {
$cmd->execute();
}
}
}
package jobs
type Command interface {
Execute() error
}
type SendInvoiceCommand struct {
OrderID int
Mailer Mailer
}
func (c SendInvoiceCommand) Execute() error {
return c.Mailer.SendInvoice(c.OrderID)
}
type ResizeAvatarCommand struct {
Path string
Images ImageProcessor
}
func (c ResizeAvatarCommand) Execute() error {
return c.Images.Resize(c.Path, 200, 200)
}
type JobWorker struct {
queue []Command
}
func NewJobWorker(queue []Command) *JobWorker {
return &JobWorker{queue: queue}
}
func (w *JobWorker) Run() error {
for len(w.queue) > 0 {
cmd := w.queue[0]
w.queue = w.queue[1:]
if err := cmd.Execute(); err != nil {
return err
}
}
return nil
}
The JobWorker doesn't import Mailer or ImageProcessor. It doesn't know what kind of work it's running. It just pulls objects off a queue and asks them to do their thing. Adding a fourth job is one new class — the worker stays untouched.
When to Use It
Reach for Command when you can answer "yes" to any of these:
- You want to queue work for later. Background jobs, scheduled tasks, "send this in 5 minutes" — anything where the request needs to outlive the moment it was made.
- You want to log requests. When the request is an object, you can serialize it and replay it later. Audit trails, command-sourcing, debugging "what did the user ask for?" — all of these get easier.
- You want undo / redo. If your Concrete Commands also implement
undo(), you have a reversible operation system. Editors, design tools, anything with an "oops" button. - You want to retry failures. A failed command can be re-enqueued because it's still the same object with all its data.
- You want to parameterize triggers. A button, a menu item, a keyboard shortcut, a webhook — all just need "an object with
execute()." The trigger doesn't care which command.
If you only need one of these things and your call is fast and synchronous, just call the method.
Pros and Cons
Pros
- The Invoker (worker, button, scheduler) is decoupled from the Receivers (services that do the work).
- Requests become first-class data — queueable, loggable, retryable, undoable.
- Adding new commands doesn't touch existing ones.
- Each command is small, focused, and testable in isolation.
Cons
- More classes. Every action becomes a class — that's a lot of files in a busy app.
- Serialization gets fiddly when commands hold service references (you usually serialize the data and reconstruct the dependencies on the other side).
- Undo is much harder to add later than to design in from the start. If you might need it, plan for it.
Pro Tips
- Keep the data inside the command serializable. If your queue is Redis, RabbitMQ, or a database, the command needs to round-trip as JSON or similar. Keep service references out of the serialized payload.
- Inject Receivers, don't construct them in the command. The command holds what to do; the worker resolves how to do it via the container.
- Use one method,
execute(). Resist the urge to addvalidate(),prepare(),summarize(). If you need them, they're separate concerns — give them their own home. - For undo, store what changed — not the old object. A diff is cheap to keep around; a full clone is not.
Relations with Other Patterns
- Strategy is Command's lighter sibling — both wrap behavior in an object, but Strategy is picked by the client at call-time, while Command is constructed once and passed around.
- State pairs naturally with Command: each transition can be a Command, giving you a complete audit log of "who did what to push this through the lifecycle."
- Event Sourcing is Command taken to its logical conclusion: every change is a command, persisted forever, and the current state is derived by replaying them.
- CQRS uses Command on the write side (commands change state) and Query objects on the read side (queries return data) — a clean way to scale reads and writes independently.
Final Tips
The first time Command paid me back was in a job system that needed to retry exactly the same work after a network blip. Without Command, "retry" meant remembering which method was called and what the arguments were — fragile, easy to lose. With Command, the queue was the memory. We just put the failed object back at the head of the line and ran the loop again.
Once requests become objects, a lot of hard problems get easy. Use this pattern when "do this later" or "do this again" or "show me what was done" become part of the requirements.


