Skip to content

Commands

A command is a message that indicates an intention to change the state of the bounded context. It is a request to perform an action that will result in a change to the bounded context's state. For example, "create a new order", "remove an attendee from an event", "update a customer's details".

Command messages define the data contract for the information that needs to be provided to perform the action. They are dispatched to the command bus, and executed by command handlers.

Command Messages

Command messages are defined by writing a class that implements the Command interface. The class should be named according to the action it represents, and should contain properties that represent the data required to perform the action. I.e. it defines the data contract for the action. Commands must be immutable.

For example:

php
namespace App\Modules\EventManagement\Application\UseCases\Commands\CancelAttendeeTicket;

use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier;
use CloudCreativity\Modules\Contracts\Toolkit\Messages\Command;
use VendorName\EventManagement\Shared\Enums\CancellationReasonEnum;

final readonly class CancelAttendeeTicketCommand implements Command
{
    public function __construct(
        public Identifier $attendeeId,
        public Identifier $ticketId,
        public CancellationReasonEnum $reason,
    ) {
    }
}

TIP

Some commands will only need to hold a few values to perform the action - such as in the example above, where the action can be described by two identifiers and an enum.

Other commands might have significantly more complex data as their action input. It's good practice to make good use of value objects and "write models" to describe the data required by the command.

Command Handlers

A command handler is a class that is responsible for performing the action described by a command. It is a use case in the application layer of the bounded context. The command handler is responsible for validating the command, performing the action, and updating the state of the bounded context.

For example:

php
namespace App\Modules\EventManagement\Application\UseCases\Commands\CancelAttendeeTicket;

use App\Modules\EventManagement\Application\Ports\Driven\Persistence\AttendeeRepository;
use CloudCreativity\Modules\Application\Bus\Middleware\ExecuteInUnitOfWork;
use CloudCreativity\Modules\Contracts\Application\Messages\DispatchThroughMiddleware;
use CloudCreativity\Modules\Toolkit\Results\Result;

final readonly class CancelAttendeeTicketHandler implements
    DispatchThroughMiddleware
{
    public function __construct(
        private AttendeeRepository $attendees,
    ) {
    }

    public function handle(CancelAttendeeTicketCommand $command): Result
    {
        $attendee = $this->attendees->findOrFail($command->attendeeId);

        if (!$attendee->hasTicket($command->ticketId)) {
            return Result::failed('The attendee does not have the specified ticket.');
        }

        $attendee->cancelTicket(
            $command->ticketId,
            $command->reason,
        );

        $this->attendees->update($attendee);

        return Result::ok();
    }

    public function middleware(): array
    {
        return [
            ExecuteInUnitOfWork::class,
        ];
    }
}

TIP

You'll notice from the example above that our command handlers support middleware. In this example, we are ensuring that the handler executes within a unit of work - i.e. the action is performed within a single transaction.

Middleware is optional - if you do not need to use any middleware specific to the handler, your handler does not need to implement the DispatchThroughMiddleware interface.

Results

We use a result object pattern for returning the outcome of the command - see the Results chapter for information on using this object.

This is a simple object that indicates whether the action was successful or not, and can contain additional information about the outcome. This is a good practice because it makes it clear to the caller what the outcome of the action was, and allows the caller to handle the outcome appropriately.

Commands must never return the state of the system. A command is a request to change the state, and a query should be used to retrieve the state.

This means typically you should not return too much information in the result object - unless you need to return something that is specific to the action that was performed. For example, if the action was to create a new entity, you would need to return the resulting identifier in the result. That would then allow the calling code to dispatch a query to retrieve the state of the newly created entity, if desired.

Command Bus

To allow the outside world to execute commands, our bounded context must expose a command bus as a driving port. Although there is a generic command bus interface, our bounded context needs to expose its specific command bus.

Command Bus Port

We do this by defining an interface in our application's driving ports.

php
namespace App\Modules\EventManagement\Application\Ports\Driving;

use CloudCreativity\Modules\Application\Ports\Driving\CommandDispatcher;

interface CommandBus extends CommandDispatcher
{
}

And then our implementation is as follows:

php
namespace App\Modules\EventManagement\Application\Bus;

use App\Modules\EventManagement\Application\Ports\Driving\CommandBus as Port;
use CloudCreativity\Modules\Application\Bus\CommandDispatcher;

final class CommandBus extends CommandDispatcher implements Port
{
}

Creating a Command Bus

The command dispatcher class that your implementation extends (in the above example) allows you to build a command bus specific to your domain. You do this by:

  1. Binding command handler factories into the command dispatcher; and
  2. Binding factories for any middleware used by your bounded context; and
  3. Optionally, attaching middleware that runs for all commands dispatched through the command bus.

Factories must always be lazy, so that the cost of instantiating command handlers or middleware only occurs if the handler or middleware are actually being used.

For example:

php
namespace App\Modules\EventManagement\Application\Bus;

use App\Modules\EventManagement\Application\UsesCases\Commands\{
    CancelAttendeeTicket\CancelAttendeeTicketCommand,
    CancelAttendeeTicket\CancelAttendeeTicketHandler,
};
use App\Modules\EventManagement\Application\Ports\Driving\CommandBus as CommandBusPort;
use App\Modules\EventManagement\Application\Ports\Driven\DependencyInjection\ExternalDependencies;
use CloudCreativity\Modules\Application\Bus\CommandHandlerContainer;
use CloudCreativity\Modules\Application\Bus\Middleware\ExecuteInUnitOfWork;
use CloudCreativity\Modules\Application\Bus\Middleware\LogMessageDispatch;
use CloudCreativity\Modules\Toolkit\Pipeline\PipeContainer;

final class CommandBusProvider
{
    public function __construct(
        private readonly ExternalDependencies $dependencies,
    ) {
    }

    public function getCommandBus(): CommandBusPort
    {
        $bus = new CommandBus(
            handlers: $handlers = new CommandHandlerContainer(),
            middleware: $middleware = new PipeContainer(),
        );

        /** Bind commands to handler factories */
        $handlers->bind(
            CancelAttendeeTicketCommand::class,
            fn() => new CancelAttendeeTicketHandler(
                $this->dependencies->getAttendeeRepository(),
            ),
        );

        /** Bind middleware factories */
        $middleware->bind(
            ExecuteInUnitOfWork::class,
            fn () => new ExecuteInUnitOfWork($this->dependencies->getUnitOfWorkManager()),
        );

        $middleware->bind(
            LogMessageDispatch::class,
            fn () => new LogMessageDispatch(
                $this->dependencies->getLogger(),
            ),
        );

        /** Attach middleware that runs for all commands */
        $bus->through([
            LogMessageDispatch::class,
        ]);

        return $bus;
    }
}

Adapters in the presentation and delivery layer will use the driving ports. Typically this means we need to bind the port into a service container. For example, in Laravel:

php
namespace App\Providers;

use App\Modules\EventManagement\Application\{
    Bus\CommandBusProvider,
    Ports\Driving\CommandBus,
};
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\ServiceProvider;

final class EventManagementServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind(
            CommandBus::class,
            static function (Container $app)  {
                $provider = $app->make(CommandBusProvider::class);
                return $provider->getCommandBus();
            },
        );
    }
}

Dispatching Commands

You can now dispatch command messages to your bounded context from the outside world. For example, if we were using a single action controller to handle a HTTP request in a Laravel application, we might dispatch a command like this:

php
namespace App\Http\Controllers\Api\Attendees;

use App\Modules\EventManagement\Application\{
    Ports\Driving\CommandBus,
    UseCases\Commands\CancelAttendeeTicket\CancelAttendeeTicketCommand,
};
use CloudCreativity\Modules\Toolkit\Identifiers\IntegerId;
use Illuminate\Validation\Rule;
use VendorName\EventManagement\Shared\Enums\CancellationReasonEnum;

class CancellationController extends Controller
{
    public function __invoke(
        Request $request,
        CommandBus $bus,
        string $attendeeId,
    ) {
        $validated = $request->validate([
            'ticket' => ['required', 'integer'],
            'reason' => ['required', Rule::enum(CancellationReasonEnum::class)]
        ]);

        $command = new CancelAttendeeTicketCommand(
            attendeeId: new IntegerId((int) $attendeeId),
            ticketId: new IntegerId((int) $validated['ticket']),
            reason: CancellationReasonEnum::from($request['reason']),
        );

        $result = $bus->dispatch($command);

        if ($result->didFail()) {
            // or throw here, if we are not expecting a failure to occur.
            return response()->json([
                'message' => 'The attendee ticket could not be cancelled.',
            ], 422);
        }

        return response()->noContent();
    }
}

TIP

Here you can see that the event management bounded context is entirely encapsulated. The outside world uses the combination of the command message, with the driving port it needs to dispatch the message. Everything else - how your bounded context processes and responds to the action - is hidden as an internal implementation detail of your domain.

Command Queuer

The command bus dispatches the bounded context's logic immediately via a command handler. Or in other words - commands are dispatched synchronously. But what happens if the presentation and delivery layer does not need to wait for the result of the command being dispatched, and instead wants the command to be handled in a non-blocking way?

In this scenario, the presentation and delivery layer would need to queue the command for asynchronous processing. It would do this via a command queuer, which is provided by your bounded context as a driving port.

TIP

For more information on asynchronous processing patterns, see the Asynchronous Processing chapter.

Command Queuer Port

To allow the outside world to queue commands, our bounded context must expose a command queuer as a driving port. Although there is a generic command queuer interface, our bounded context needs to expose its specific command queuer.

We do this by defining an interface in our application's driving ports.

php
namespace App\Modules\EventManagement\Application\Ports\Driving;

use CloudCreativity\Modules\Application\Ports\Driving\CommandQueuer as ICommandQueuer;

interface CommandQueuer extends ICommandQueuer
{
}

And then our implementation is as follows:

php
namespace App\Modules\EventManagement\Application\Bus;

use App\Modules\EventManagement\Application\Ports\Driving\CommandQueuer as Port;
use App\Modules\EventManagement\Application\Ports\Driven\Queue;
use CloudCreativity\Modules\Application\Bus\CommandQueuer as Queuer;

final class CommandQueuer extends Queuer implements Port
{
    public function __construct(Queue $queue)
    {
        parent::__construct($queue);
    }
}

Notice that the command queuer dependency injects the specific queue instance for this bounded context - which is a driven port. This is because it expects to queue commands not on any queue, but on the specific queue for this bounded context. I.e. it follows the encapsulation principle.

See the Queue chapter for more information on how to implement a queue.

Creating a Command Queuer

Creating a command queuer is simple, as it is just a thin wrapper around the queue - i.e. it immediately hands off to a driven port. This is because queuing a command is an infrastructure concern.

php
namespace App\Modules\EventManagement\Application\Bus;

use App\Modules\EventManagement\Application\Ports\Driving\CommandQueuer as CommandQueuerPort;
use App\Modules\EventManagement\Application\Ports\Driven\DependencyInjection\ExternalDependencies;

final class CommandBusProvider
{
    public function __construct(
        private readonly ExternalDependencies $dependencies,
    ) {
    }

    public function getCommandQueuer(): CommandQueuerPort
    {
        return new CommandQueuer(
            queue: $this->dependencies->getQueue(),
        );
    }
}

TIP

The queue supports middleware to add cross-cutting concerns, such as logging. This means there is no need to add any middleware to the command queuer.

Queuing Commands

The command queuer can be used to execute a command in a non-blocking way. For example, our controller implementation from earlier in this chapter could be updated to return a 202 Accepted response to indicate the command has been queued:

php
namespace App\Http\Controllers\Api\Attendees;

use App\Modules\EventManagement\Application\{
    Ports\Driving\CommandQueuer,
    UseCases\Commands\CancelAttendeeTicket\CancelAttendeeTicketCommand,
};
use CloudCreativity\Modules\Toolkit\Identifiers\IntegerId;
use Illuminate\Validation\Rule;
use VendorName\EventManagement\Shared\Enums\CancellationReasonEnum;

class CancellationController extends Controller
{
    public function __invoke(
        Request $request,
        CommandQueuer $bus,
        string $attendeeId,
    ) {
        $validated = $request->validate([
            'ticket' => ['required', 'integer'],
            'reason' => ['required', Rule::enum(CancellationReasonEnum::class)]
        ]);

        $command = new CancelAttendeeTicketCommand(
            attendeeId: new IntegerId((int) $attendeeId),
            ticketId: new IntegerId((int) $validated['ticket']),
            reason: CancellationReasonEnum::from($request['reason']),
        );

        $bus->queue($command);

        return response()->noContent(status: 202);
    }
}

Middleware

Our command bus implementation gives you complete control over how to compose the handling of your commands, via middleware. Middleware is a powerful way to add cross-cutting concerns to your command handling, such as logging, transaction management, and so on.

Middleware can be added either to the command bus (so it runs for every command) or to individual command handlers.

To apply middleware to the command bus, you can use the through() method on the bus - as shown in the example above. Middleware is executed in the order it is added to the bus.

To apply middleware to a specific command handler, the handler must implement the DispatchThroughMiddleware interface, as shown in the example handler above. The middleware() method should return an array of middleware to run, in the order they should be executed. Handler middleware are always executed after the bus middleware.

This package provides a number of command middleware, which are described below. Additionally, you can write your own middleware to suit your specific needs.

Setup and Teardown

Our SetupBeforeDispatch middleware allows setup work to be run before the command is dispatched, and optionally teardown work when the command has completed.

This allows you to set up any state and guarantee that the state is cleaned up, regardless of the outcome of the command. The primary use case for this is to boostrap Domain Services and to garbage collect any singleton instances of dependencies.

For example:

php
use App\Modules\EventManagement\Domain\Services;
use CloudCreativity\Modules\Application\Bus\Middleware\SetupBeforeDispatch;

$middleware->bind(
    SetupBeforeDispatch::class,
    fn () => new SetupBeforeDispatch(function (): Closure {
        // setup domain services
        Services::setEvents(fn() => $this->getDomainEventDispatcher());
        return function (): void {
            // clean up a singleton instance of a unit of work manager.
            $this->unitOfWorkManager = null;
            // teardown the domain services
            Services::tearDown();
        };
    }),
);

$bus->through([
    LogMessageDispatch::class,
    SetupBeforeDispatch::class,
]);

Here our setup middleware takes a setup closure as its only constructor argument. This setup closure can optionally return a closure to do any teardown work. The teardown callback is guaranteed to always be executed, regardless of whether a command succeeded or failed - and it also runs if an exception is thrown.

If you only need to do teardown work, use the TeardownAfterDispatch middleware instead. This takes a single teardown closure as its only constructor argument:

php
use CloudCreativity\Modules\Application\Bus\Middleware\TeardownAfterDispatch;

$middleware->bind(
    TeardownAfterDispatch::class,
    fn () => new TeardownAfterDispatch(function (): Closure {
        // clean up a singleton instance of a unit of work manager.
        $this->unitOfWorkManager = null;
    }),
);

$bus->through([
    LogMessageDispatch::class,
    TearDownAfterDispatch::class,
]);

Unit of Work

Ideally command handlers should always be executed in a unit of work. We cover this in detail in the Units of Work chapter.

To execute a handler in a unit of work, you will need to use our ExecuteInUnitOfWork middleware. You should always implement this as handler middleware - because typically you need it to be the final middleware that runs before a handler is invoked. It also makes it clear to developers looking at the command handler that it is expected to run in a unit of work. The example CancelAttendeeTicketHandler above demonstrates this.

An example binding for this middleware is:

php
use CloudCreativity\Modules\Application\Bus\Middleware\ExecuteInUnitOfWork;

$middleware->bind(
    ExecuteInUnitOfWork::class,
    fn () => new ExecuteInUnitOfWork(
        $this->dependencies->getUnitOfWorkManager(),
    ),
);

WARNING

If you're using a unit of work, you should be combining this with our "unit of work domain event dispatcher". One really important thing to note is that you must inject both the middleware and the domain event dispatcher with exactly the same instance of the unit of work manager.

I.e. use a singleton instance of the unit of work manager. Plus use the teardown middleware (described above) to dispose of the singleton instance once the command has executed.

Flushing Deferred Events

If you are not using a unit of work, you will most likely be using our deferred domain event dispatcher. This is covered in the Domain Events chapter.

When using this dispatcher, you will need to use our FlushDeferredEvents middleware. You should always implement this as handler middleware - because typically you need it to be the final middleware that runs before a handler is invoked. I.e. this is an equivalent middleware to the unit of work middleware.

An example binding for this middleware is:

php
use CloudCreativity\Modules\Application\Bus\Middleware\FlushDeferredEvents;

$middleware->bind(
    FlushDeferredEvents::class,
    fn () => new FlushDeferredEvents(
        $this->eventDispatcher,
    ),
);

WARNING

When using this middleware, it is important that you inject it with a singleton instance of the deferred event dispatcher. This must be the same instance that is exposed to your domain layer as a service.

Logging

Use our LogMessageDispatch middleware to log the dispatch of a command, and the result. The middleware takes a PSR Logger.

php
use CloudCreativity\Modules\Application\Bus\Middleware\LogMessageDispatch;

$middleware->bind(
    LogMessageDispatch::class,
    fn (): LogMessageDispatch => new LogMessageDispatch(
        $this->dependencies->getLogger(),
    ),
);

$bus->through([LogMessageDispatch::class]);

The middleware will log a message before executing the command, with a log level of debug. It will then log a message after the command has executed, with a log level of info.

You can adjust the log levels via constructor arguments. For example, if we wanted the before dispatch message to be info, and the dispatched message to be notice:

php
use Psr\Log\LogLevel;

$middleware->bind(
    LogMessageDispatch::class,
    fn (): LogMessageDispatch => new LogMessageDispatch(
        logger: $this->dependencies->getLogger(),
        dispatchLevel: LogLevel::INFO,
        dispatchedLevel: LogLevel::NOTICE,
    ),
);

$bus->through([LogMessageDispatch::class]);

Log Context

When logging that the command is being dispatched, we log all the public properties of the command message as log context. This is useful for debugging, as it allows you to see the data that was provided.

However, there may be scenarios where a property should not be logged, e.g. because it contains sensitive information. In this scenario, use the Sensitive attribute on the property, and it will not be logged:

php
use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier;
use CloudCreativity\Modules\Contracts\Toolkit\Messages\Command;
use CloudCreativity\Modules\Toolkit\Loggable\Sensitive;

final readonly class CancelAttendeeTicketCommand implements Command
{
    public function __construct(
        public Identifier $attendeeId,
        #[Sensitive] public Identifier $ticketId,
        public CancellationReasonEnum $reason,
    ) {
    }
}

If you need full control over the log context, implement the ContextProvider interface on your command message:

php
use CloudCreativity\Modules\Contracts\Toolkit\Identifiers\Identifier;
use CloudCreativity\Modules\Contracts\Toolkit\Loggable\ContextProvider;
use CloudCreativity\Modules\Contracts\Toolkit\Messages\Command;

final readonly class CancelAttendeeTicketCommand implements
  Command,
  ContextProvider
{
    public function __construct(
        public Identifier $attendeeId,
        public Identifier $ticketId,
        public CancellationReasonEnum $reason,
    ) {
    }

    public function context(): array
    {
        return [
            'attendeeId' => $this->attendeeId,
        ];
    }
}

Writing Middleware

You can write your own middleware to suit your specific needs. Middleware is a simple invokable class, with the following signature:

php
namespace App\Modules\EventManagement\Application\Bus\Middleware;

use Closure;
use CloudCreativity\Modules\Contracts\Application\Bus\CommandMiddleware;
use CloudCreativity\Modules\Contracts\Toolkit\Messages\Command;
use CloudCreativity\Modules\Contracts\Toolkit\Result\Result;

final class MyMiddleware implements CommandMiddleware
{
    /**
     * Execute the middleware.
     *
     * @param Command $command
     * @param Closure(Command): Result<mixed> $next
     * @return Result<mixed>
     */
    public function __invoke(
        Command $command, 
        Closure $next,
    ): Result
    {
        // code here executes before the handler

        $result = $next($command);

        // code here executes after the handler

        return $result;
    }
}

TIP

If you're writing middleware that is only meant to be used for a specific command, do not implement the CommandMiddleware interface. Instead, use the same signature but change the type-hint for the command to the command class your middleware is designed to be used with.

If you want to write middleware that can be used with both commands and queries, implement the BusMiddleware interface instead:

php
namespace App\Modules\EventManagement\Application\Bus\Middleware;

use Closure;
use CloudCreativity\Modules\Contracts\Application\Bus\BusMiddleware;
use CloudCreativity\Modules\Contracts\Toolkit\Messages\Command;
use CloudCreativity\Modules\Contracts\Toolkit\Messages\Query;
use CloudCreativity\Modules\Contracts\Toolkit\Result\Result;

class MyBusMiddleware implements BusMiddleware
{
    /**
     * Handle the command or query.
     *
     * @param Command|Query $message
     * @param Closure(Command|Query): Result<mixed> $next
     * @return Result<mixed>
     */
    public function __invoke(
        Command|Query $message, 
        Closure $next,
    ): Result
    {
        // code here executes before the handler

        $result = $next($command);

        // code here executes after the handler

        return $result;
    }
}

Released under the MIT License.