Command Pattern
Laravel: Laravel Jobs (ShouldQueue), Bus::dispatch(), Artisan Commands
Command Pattern
The Real-World Analogy
Think about how refunds work at a payment kiosk. A customer walks up and requests a refund for a failed top-up. The kiosk operator doesn't immediately reach into the cash drawer and hand over money. Instead, they fill out a refund slip -- a physical document that captures everything about the request: the original transaction ID, the amount, the customer, and the reason. That slip goes into a queue. A supervisor reviews it. If approved, it gets executed. If something goes wrong, the slip provides a paper trail to reverse the action.
That refund slip is a Command object. It encapsulates a request as a self-contained object, carrying everything needed to execute the operation. The slip can be queued, logged, validated, and even reversed -- all because the request was turned into a thing rather than an immediate action.
The Command pattern says: don't call operations directly. Wrap them in objects that can be stored, passed around, and executed later.
The Anti-Pattern: Inline Execution With No Undo
Here's what happens when you skip the Command pattern. A refund controller that directly manipulates balances:
class RefundController
{
public function processRefund(int $transactionId, float $amount): void
{
// Directly modifying state -- no way to undo, log, or replay
$account = AccountStore::find($transactionId);
$account->balance += $amount;
$account->save();
echo "Refunded {$amount} to account.\n";
// Another direct call -- what if this fails after the balance changed?
$ledger = new Ledger();
$ledger->recordCredit($transactionId, $amount);
// And another -- if SMS fails, is the refund still valid?
SmsGateway::send($account->phone, "Your refund of {$amount} has been processed.");
}
}
Why this is a problem:
- No undo capability. Once the balance is changed, there is no structured way to reverse it. You'd have to write a separate "un-refund" method with its own logic.
- No history. There is no record of what was requested, only the side effects left behind. If something fails halfway through, you have a partial state with no trail.
- No queuing. Every refund executes immediately in the request cycle. You cannot batch refunds, defer them, or retry failed ones.
- Tight coupling. The controller knows about account storage, ledger recording, and SMS sending. Adding a new side effect (email notification, audit log) means modifying this method.
- No replay. If you need to reprocess yesterday's refunds after a system failure, there is nothing to replay from.
The Fix: Encapsulate as Command Objects
Define a command interface and wrap each operation in a command object:
interface CommandInterface
{
public function execute(): void;
public function undo(): void;
}
class ProcessRefundCommand implements CommandInterface
{
public function __construct(
private Account $account,
private float $amount,
) {}
public function execute(): void
{
$this->account->credit($this->amount);
}
public function undo(): void
{
$this->account->debit($this->amount);
}
}
Now the refund is an object. You can store it, log it, queue it, and undo it:
$command = new ProcessRefundCommand($account, 150.00);
// Execute it
$command->execute(); // Account credited
// Changed your mind? Undo it
$command->undo(); // Account debited back
Key Roles in the Command Pattern
The pattern has four participants:
- Command -- the interface declaring
execute()(and optionallyundo()). Every concrete command implements this. - Concrete Command -- a specific operation wrapped as an object (
ProcessRefundCommand,TransferCommand). It holds a reference to the Receiver and calls methods on it. - Receiver -- the object that actually does the work (
Account,Ledger). The receiver does not know it is being called through a command. - Invoker -- the object that triggers commands (
CommandProcessor,TransactionQueue). It does not know what the command does -- it just callsexecute(). It can also maintain a history stack for undo support.
The Invoker: Command Processor
The invoker decouples "when to execute" from "what to execute":
class CommandProcessor
{
private array $history = [];
public function run(CommandInterface $command): void
{
$command->execute();
$this->history[] = $command;
}
public function undoLast(): void
{
$command = array_pop($this->history);
if ($command !== null) {
$command->undo();
}
}
}
The processor can execute any command without knowing its internals. It stores a history, enabling undo. You could extend this to support redo, batching, transaction logging, or deferred execution -- all without changing the commands themselves.
GoF Intent
The Gang of Four defines the Command pattern as: "Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations."
Every word of that definition maps to something practical:
- Parameterize clients -- the invoker accepts any command through the same interface.
- Queue requests -- commands are objects, so they can be stored in arrays, queues, or databases.
- Log requests -- you can serialize a command or record its parameters for audit.
- Undoable operations -- the
undo()method reversesexecute().
When to Use It
Use the Command pattern when you need to:
- Support undo/redo in a transaction processing system
- Queue operations for later execution (batch refunds, deferred settlements)
- Log every operation for audit trails (financial compliance)
- Decouple the object that invokes an operation from the object that performs it
- Implement macro commands (a sequence of commands executed as one)
Key takeaway: When you find yourself executing operations directly and wishing you could undo, replay, or queue them, wrap those operations in Command objects. The operation becomes data -- and data can be stored, reversed, and managed.
What You Learned
- The Command pattern encapsulates a request as an object with
execute()and optionallyundo()methods - Commands decouple "what to do" from "when and how to do it" — enabling queues, logging, and replay
- An invoker stores and triggers commands without knowing what they actually do
- Undo support comes naturally: each command knows how to reverse its own action
Command Pattern in Laravel
Laravel is built on the Command pattern in multiple places. The most prominent examples are Jobs and Artisan Commands -- both are request objects that encapsulate operations and can be executed, queued, serialized, and retried.
Laravel Jobs Are Commands
A Laravel Job is a textbook Command object. It encapsulates an operation in a class with a handle() method (the equivalent of execute()):
// app/Jobs/ProcessRefund.php
namespace App\Jobs;
use App\Models\Transaction;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class ProcessRefund implements ShouldQueue
{
use Dispatchable, Queueable;
public function __construct(
private Transaction $transaction,
private float $amount,
) {}
// This IS the execute() method
public function handle(): void
{
$this->transaction->account->credit($this->amount);
$this->transaction->update(['status' => 'refunded']);
}
// Laravel even supports failure handling
public function failed(\Throwable $exception): void
{
// Log the failure, notify admin, etc.
}
}
Dispatching this job is exactly like handing a command to an invoker:
// In a controller -- the controller is the invoker
ProcessRefund::dispatch($transaction, 150.00);
// You can queue it, delay it, chain it
ProcessRefund::dispatch($transaction, 150.00)
->onQueue('refunds')
->delay(now()->addMinutes(5));
The controller does not know how the refund is processed. It creates the command object and hands it off. The queue worker (Laravel's invoker) executes it later.
Bus::dispatch() -- The Invoker
Laravel's Illuminate\Bus\Dispatcher is the invoker in the Command pattern. It receives command objects and decides how to execute them:
use Illuminate\Support\Facades\Bus;
// Dispatch a single command
Bus::dispatch(new ProcessRefund($transaction, 150.00));
// Dispatch a chain of commands (macro command)
Bus::chain([
new ValidateRefund($transaction),
new ProcessRefund($transaction, 150.00),
new SendRefundNotification($transaction),
])->dispatch();
// Dispatch a batch of commands
Bus::batch([
new ProcessRefund($transaction1, 50.00),
new ProcessRefund($transaction2, 75.00),
new ProcessRefund($transaction3, 120.00),
])->dispatch();
Chaining and batching are natural extensions of the Command pattern -- because each operation is an independent object, they can be composed, ordered, and grouped.
Artisan Commands
Laravel's Artisan console commands are another implementation of the Command pattern. Each command encapsulates a console operation:
// app/Console/Commands/ProcessDailySettlements.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class ProcessDailySettlements extends Command
{
protected $signature = 'settlements:process {date?}';
protected $description = 'Process all pending settlements for a given date';
// execute()
public function handle(): int
{
$date = $this->argument('date') ?? now()->toDateString();
$this->info("Processing settlements for {$date}...");
// Settlement logic here
$count = $this->processSettlements($date);
$this->info("Processed {$count} settlements.");
return Command::SUCCESS;
}
private function processSettlements(string $date): int
{
// ...
return 42;
}
}
The Artisan kernel is the invoker. It resolves the right command based on the signature and calls handle(). You can also invoke commands programmatically:
// Call a command from code -- invoking a command object
Artisan::call('settlements:process', ['date' => '2024-01-15']);
// Queue it for background execution
Artisan::queue('settlements:process', ['date' => '2024-01-15']);
Queueable Trait -- Serialization for Deferred Execution
The Illuminate\Bus\Queueable trait enables commands to be serialized and stored in a queue backend (Redis, SQS, database). This is the "queue or log requests" part of the GoF definition:
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
class ProcessRefund implements ShouldQueue
{
use Queueable, SerializesModels;
// SerializesModels ensures Eloquent models are serialized
// as their ID and re-fetched when the job executes.
// This prevents stale data and reduces payload size.
}
When you dispatch this job, Laravel serializes the command object (including its constructor arguments), stores it in the queue, and a worker process deserializes and executes it later. The command travels across process boundaries as data.
Event-Driven Command Execution
Laravel's event system can trigger commands, creating a reactive pipeline:
// When a refund is requested, dispatch the command
Event::listen(RefundRequested::class, function (RefundRequested $event) {
ProcessRefund::dispatch($event->transaction, $event->amount);
});
This combines the Observer pattern (Module 11) with the Command pattern -- events trigger commands, commands encapsulate operations.
Failed Job Handling -- Graceful Degradation
Laravel provides built-in support for command failure, which maps to error handling in the Command pattern:
// config/queue.php defines failed job storage
// Failed jobs are stored and can be retried:
// List failed commands
// php artisan queue:failed
// Retry a specific failed command
// php artisan queue:retry 5
// Retry all failed commands
// php artisan queue:retry all
This is logging and replay built into the framework's command infrastructure.
Key takeaway: Laravel's Job and Artisan systems are the Command pattern made concrete. handle() is execute(). Bus::dispatch() is the invoker. ShouldQueue enables deferred execution. Every time you create a Job class in Laravel, you are implementing the Command pattern -- whether you realize it or not.
This example is pre-filled and runnable. Click "Run" to see the output. Feel free to modify the code and experiment.
Complete the code below to pass all test cases. Use the hints if you get stuck.