Command Bus implementation

Posted Sunday 4th February, 2024

By Rob Watson

Laravel CQRS

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:

None of which quite felt "right":

This CommandBus implementation solves all of those problems by giving a simple interface to send commands through for them to be handled.