Command Bus implementation
Posted Sunday 4th February, 2024
By Rob Watson
As part of building Source Pot, I'm building a rough CQRS
(Command-Query Responsibility Segregation) setup so I can separate read and write streams (which I may do by using Dynamo DB for reading, and MySQL for writing, then synchronise the two).
I aim to make it simple yet clear, and fit neatly into Laravel's ecosystem. I'll also try to lean on SOLID principles for it.
Command
:
namespace Framework\Contracts\CommandBus;
interface Command {}
A Command
is a simple holder of data. It can be thought of like a DTO, a "plain object", etc. So the interface is simple. The reason I'm using an interface at all is so we can type-hint that in other functions.
An example command:
namespace Auth\Commands;
final readonly class RegisterUserCommand implements Command
{
public function __construct(
public string $email,
public string $name,
public string $password,
) {}
}
Command Handler
:
namespace Framework\Contracts\CommandBus;
interface CommandHandler
{
public function handle(Command $command): void;
}
The Handler
is also a simple device. It has a single method that accepts a Command, returning nothing.
Example Handler:
namespace Auth\Commands;
use Framework\Contracts\CommandBus\CommandHandler;
final readonly class RegisterUserHandler implements CommandHandler
{
public function __construct(
private UserRepository $repository,
) {}
public function handle(Command $command): void
{
$this->repository->createUser(
email: $command->email,
name: $command->name,
password: $command->password,
);
}
}
The way these Handlers will be "built" in the application will be through Laravel's App Container so we can have injected dependencies in the constructor, then our handle method just accepts the command itself.
Helpers
Before we get into the actual Command Bus, we need to do some more work to find a Handler for a Command, then build the Handler instance.
You may notice the namespace is the same for both Command
and Handler
. This is because to start with I'll just infer the name of the handler class with a simple string replace.
Following our SOLID principles (Dependency Inversion, in particular), we'll whip up an interface first:
namespace Framework\Contracts\CommandBus;
interface HandlerLocator
{
public function locate(Command $command): string;
}
namespace Framework\CommandBus;
use Framework\Contracts\CommandBus\Command;
use Framework\Contracts\CommandBus\Handler;
use Framework\Contracts\CommandBus\HandlerLocator;
final class StringReplaceHandlerLocator implements HandlerLocator
{
public function locate(Command $command): string
{
// Changes "\Auth\Commands\RegisterUserCommand" to "\Auth\Commands\RegisterUserHandler"
return preg_replace('#(.*)Command$#', '$1Handler', get_class($command));
}
}
Then we need a way to create an instance of the handler using Laravel's Container:
namespace Framework\Contracts\CommandBus;
interface HandlerInstantiator
{
public function make(string $handlerClass): CommandHandler;
}
namespace Framework\CommandBus;
use Framework\Contracts\CommandBus\HandlerInstantiator;
use Framework\Contracts\CommandBus\CommandHandler;
final class LaravelHandlerInstantiator implements HandlerInstantiator
{
public function make(string $handlerClass): CommandHandler
{
return app()->make($handlerClass);
}
}
By using these helper classes we can easily change the implementation of our Command Bus just by swapping these out for new versions with different behaviour without touching the actual Command Bus class.
The CommandBus
Speaking of the Command Bus, this is now a fairly trivial class:
namespace Framework\Contracts\CommandBus;
interface CommandBus
{
public function execute(Command $command): void;
}
namespace Framework\CommandBus;
use Framework\Contracts\CommandBus\Command;
use Framework\Contracts\CommandBus\CommandBus as CommandBusInterface;
use Framework\Contracts\CommandBus\Handler;
use Framework\Contracts\CommandBus\HandlerInstantiator;
use Framework\Contracts\CommandBus\HandlerLocator;
final class CommandBus implements CommandBusInterface
{
public function __construct(
private HandlerLocator $locator,
private HandlerInstantiator $instantiator,
) {}
public function execute(Command $command): void
{
$this->handler($command)->handle($command);
}
private function handler(Command $command): Handler
{
$handler = $this->locator->locate($command);
return $this->instantiator->make($handler);
}
}
The execute
method is our public API, and it uses a local helper to build the Command Handler before calling the handle method.
Example usage in a Controller:
use Auth\Commands\RegisterUserCommand;
use Framework\Contracts\CommandBus\CommandBus;
class RegisterUserController
{
public function __construct(
private CommandBus $commandBus,
) {}
public function __invoke(RegisterUserRequest $request)
{
$this->commandBus->execute(new RegisterUserCommand(
name: $request->validated('name'),
email: $request->validated('email'),
password: $request->validated('password'),
));
}
}
Easy!
Final step: wiring up the dependencies
To finish this off, there's a very obvious part missing - We've used Interfaces everywhere which Laravel can't instantiate by itself. To sort this we need to create these bindings in the register
method in a Service Provider class. For my use case, this will go in my FrameworkServiceProvider
as that is responsible for booting my framework relates gubbins:
namespace Framework\Providers;
use Framework\CommandBus\CommandBus;
use Framework\CommandBus\LaravelHandlerInstantiator;
use Framework\CommandBus\StringReplaceHandlerLocator;
use Framework\Contracts\CommandBus\CommandBus as CommandBusInterface;
use Framework\Contracts\CommandBus\HandlerInstantiator;
use Framework\Contracts\CommandBus\HandlerLocator;
class FrameworkServiceProvider
{
// ... other stuff
public function register(): void
{
$this->app->bind(HandlerLocator::class, StringReplaceHandlerLocator::class);
$this->app->bind(HandlerInstantiator::class, LaravelHandlerInstantiator::class);
$this->app->bind(CommandBusInterface::class, CommandBus::class);
}
}
Now whenever I add the Command Bus dependency in a Controller Constructor (or anywhere else that's built automatically by Laravel), it'll know how to build it.
The advantages gained here should be easy to see - if I decided to change how the Handlers were built (because maybe I use my own Container), I can just create a new HandlerInstantiator class and link it here. The CommandBus cares not how its handlers are built, nor how the handlers are found.
There are also zero dependencies on Laravel in the CommandBus implementation - I could package that up and use it on any other project in the future.
Conclusion
Here you have it, a simple yet effective Command Bus implementation that supports Laravel's Container to build Handler classes so you can use all the fancy Dependency Injection that Laravel offers.
Before launching into this Command Bus, I had previously tried:
- Using Jobs and the Dispatch helper method,
- Events and Listeners,
- Creating Action classes, having them injected into my Controllers, and calling them.
None of which quite felt "right":
- Jobs with the Dispatcher helper felt too restrictive and baked-in. Calling static/helper methods isn't something I like to do as it reduces your opportunities to intercept and change behaviours.
- Events and Listeners are the wrong tools - we would fire Domain Events, not firing Commands to be listened for. Also, having to list out each Event with their Listeners AND the equivalent for Commands/Handlers would make for a big old lot of config over time.
- Action classes were my previous favourite but had all the execution login in them, again giving no opportunity to intercept them (perhaps to run them asynchronously instead).
This CommandBus implementation solves all of those problems by giving a simple interface to send commands through for them to be handled.