There exist many software design patterns but one of my favorite ones for its simplicity, flexibility and maintainability is the Command Bus. It's a pattern that comes back in many different architectures, most notably Hexagonal and Event-Driven architectures and when applying Domain-Driven Design.
In this blog post we'll delve into some of the fundamentals of the pattern and look at how to implement it with the Laravel framework.
What are commands?
In its most simple form a command is an imperative message that indicates what behavior a user, or client, wants to achieve. Events are also considered to be messages but those occur after the action has happened and are written in the past tense.
OrderCar -> CarOrdered
ScheduleAppointment
->AppointmentScheduled
In both cases, the message contains all the information needed so that the application can fulfill the command. For example, here is what the ScheduleAppointment
command could look like:
final readonly class ScheduleAppointment
{
public function __construct(
public string $patientId,
public string $doctorId,
public DatetimeInterface $scheduledAt,
) {}
}
A command doesn't know how to handle itself, that's why it also needs a command handler:
final readonly class ScheduleAppointmentHandler
{
public function handle(ScheduleAppointment $command): void
{
// Your domain logic to schedule the appointment.
}
}
And finally, you need a Command Bus that matches the command to the correct handler. This command lives in the service layer of your application and is typically called from a Controller, Console Command or Livewire component.
Most frameworks come with a command bus out of the box. Symfony has its Messenger component, or you can use Tactician for a framework-agnostic solution. For Laravel this is the Bus Facade which we'll use for the remainder of this blog post.
When to use commands
The command pattern shines when the domain you're working with has a certain amount of complexity and there is a clear process with constraints that should be followed.
Imagine that you're working on software that makes scheduling appointments at your general practitioner easier, then you can model those actions that are performed through commands such as BookAppointment
, VisitPractitioner
, etc.
If your software is primarily used for data entry and reading (CRUD) then using commands would add additional overhead and complexity. Even within a single application you could choose to approach certain bounded contexts in a CRUD or a command-oriented way.
Usage
As mentioned before, in Laravel the command bus is accessed through the Bus
facade. There are several ways you can end a command through the command bus:
Bus::dispatch(new ScheduleAppointment(...));
ScheduleAppointment::dispatch(...);
What about self-handling commands?
By default, the commands (or jobs, as Laravel calls them) created by Laravel are self-handling. Personally, I avoid doing this and instead separate the command and command handler for several reasons:
You can bypass the command bus
I've seen the following code snippet in a codebase that is using commands.
(new ScheduleAppointment())->handle();
While this works, it's not using the command bus at all and thus you lose a lot of benefits such as making it work asynchronously, or intercepting a command with middleware (more on this later).
Too tight coupling
A command is a message and should only contain the information needed to let that command be fulfilled. This also ties the command to a very specific implementation while there could be a good reason to have different handlers for the same command, as we'll explore later.
It makes dependency injection messy.
With Laravel dependencies can be injected into the handle
method of the command but this leads to some weirdly structured code when you're extracting a larger command handler into different methods.
Consider the following example where we add some constraints to our ScheduleAppointment
command handler to prevent double-bookings. The domain logic is that in order to schedule an appointment, it needs to be within the doctor's working hours and there should be no overlapping appointment.
The logic to determine this is kept in an AppointmentRepository which is injected by the handler.
final readonly class ScheduleAppointment
{
public function __construct(
public string $patientId,
public string $doctorId,
public DatetimeInterface $scheduledAt,
) {}
public function handle(AppointmentRepository $appointmentRepository): void
{
$this->guardSlotIsAvailable($appointmentRepository);
// Create the appointment
}
private function isSlotAvailable(AppointmentRepository $appointmentRepository): bool
{
if ($appointmentRepository->isOutsideOfWorkingHours($this->doctorId, $this->scheduledAt)) {
throw UnableToScheduleAppointment::outsideOfWorkingHours();
}
if ($appointmentRepository->hasOverlappingAppointment($this->doctorId, $this->scheduledAt))) {
throw UnableToScheduleAppointment::overlappingAppointment();
}
}
}
In the snippet above it feels weird that in our methods we're actually passing the dependencies instead of the actual data needed. I find that separating the handler from the command leads to more readable code:
final readonly class ScheduleAppointmentHandler
{
public function __construct(
private AppointmentRepository $appointmentRepository,
) {}
public function handle(ScheduleAppointment $command): void
{
$this->guardSlotIsAvailable($command->doctorId, $command->scheduledAt);
// Create the appointment
}
private function guardSlotIsAvailable(string $doctorId, DatetimeInterface $date): bool
{
if ($this->appointmentRepository->isOutsideOfWorkingHours($doctorId, $date)) {
throw UnableToScheduleAppointment::outsideOfWorkingHours();
}
if ($this->appointmentRepository->hasOverlappingAppointment($doctorId, $date))) {
throw UnableToScheduleAppointment::overlappingAppointment();
}
}
}
By default, the framework's command bus doesn't know how to resolve the command handler so they need to be wired up manually in a service provider's boot
method using Bus::map
.
Bus::map([
Commands\ScheduleAppointment::class => CommandHandlers\ScheduleAppointmentHandler::class,
]);
Synchronous vs asynchronous commands
An important aspect of commands and a command bus is their ability to be handled both synchronously and asynchronously. This is particularly useful for longer-running tasks that should not be blocking execution of other things (a good example is processing large file uploads).
With Laravel, this is solved out-of-the-box as every command can be handled asynchronously by implementing the ShouldQueue
interface. The documentation actually primarily focuses on the ability to queue tasks in the background which is why people often forget that they can be used in a synchronous way:
Bus::dispatchSync(new ScheduleAppointment(...));
What arguments does a command accept?
A big question I often see is which arguments a command should accept. Do we want to pass in complete objects or do we stick with scalar values? We've written about this before when discussing value objects and DTOs.
This becomes more important when you start to handle commands asynchronously and the commands get serialized onto some queuing software. Laravel does have pretty good support for this already such as automatically serializing models and other classes (notifications is another good example).
Command Middleware
Most command bus implementations offer some concept of middleware. Laravel does this as well. This allows you to add additional logic around the execution of a command.
There are several practical use cases for this such as:
- Rate limiting the execution of a command
- Dynamically swapping command handlers (based on an API version, or a feature flag)
- Preventing command handlers from overlapping for the same entity.
- Automatically adding the user that executed the command.
- Creating an audit trail.
To conclude
I think the command pattern is a great way to encapsulate domain-specific logic within your application. It helps to structure the code around the actions that are taken and allows you to also more easily test that logic.
Member discussion