Design Patterns And Data Architecture

Command Handler Pattern

The command handler pattern separates a request to do something from the object that performs it. A command is a small message describing an intended operation. A command handler receives that command and executes the operation. In PHP applications, this pattern appears in command buses, message buses, queue workers, CLI commands, Symfony Messenger, Laravel jobs, and custom application layers.

A command is not an HTTP request. It is application input. ChangeEmailCommand, CancelSubscriptionCommand, and CreateInvoiceCommand should contain the data needed for the operation, not framework request objects, database connections, or provider clients. The handler owns the use-case behavior and receives dependencies through its constructor.

The pattern is useful when the same operation can be triggered from several delivery mechanisms, when commands may be queued, when middleware around commands is valuable, or when a team wants a consistent use-case boundary. It can be overkill for a small controller that calls one clear method.

Command And Handler Roles

A command should be a simple object. It names the intent and carries input. It should be immutable where practical, often using readonly properties. It may contain already validated values, IDs, and flags. It should not perform the use case itself.

A handler performs the operation. It loads data, checks policy, calls domain behavior, saves changes, and records side effects according to the use-case design. The handler usually has one public method, commonly handle() or __invoke().

A command bus, if present, routes a command to its handler. Middleware may add logging, validation, transactions, retries, authorization, or dispatch to a queue. The bus is optional. You can use command and handler classes directly without a bus if the extra machinery is not needed.

A Small Command Handler

This example uses a command object and handler without a bus. The command carries input; the handler owns behavior.

PHP example
<?php

declare(strict_types=1);

final readonly class ChangeEmailCommand
{
    public function __construct(
        public int $userId,
        public string $newEmail,
    ) {
    }
}

final class ChangeEmailHandler
{
    public function handle(ChangeEmailCommand $command): string
    {
        if (!filter_var($command->newEmail, FILTER_VALIDATE_EMAIL)) {
            return 'Email is invalid.';
        }

        return 'Changed email for user ' . $command->userId . ' to ' . $command->newEmail . '.';
    }
}

$handler = new ChangeEmailHandler();

echo $handler->handle(new ChangeEmailCommand(42, 'ada@example.com')) . PHP_EOL;

// Prints:
// Changed email for user 42 to ada@example.com.

A production handler would load a user, check uniqueness, save the change, and record an event. The example keeps the command/handler split visible.

Commands Should Be Intent-Based

Name commands after intended business operations, not low-level technical steps. ChangeEmailCommand is better than UpdateUserTableCommand. CancelSubscriptionCommand is better than SetSubscriptionStatusCommand if cancellation has rules.

An intent-based command gives middleware, logs, metrics, and reviews a meaningful operation name. It also discourages handlers that become generic data patchers. A generic UpdateEntityCommand with arbitrary fields often bypasses business rules and makes authorization unclear.

If an operation is only a simple database maintenance task, a technical command may be fine. For application behavior, prefer business intent.

Synchronous And Asynchronous Commands

A command can be handled immediately or sent to a queue. The decision changes the contract. A synchronous command can return a result to the caller immediately. An asynchronous command may only return that work was accepted; the final outcome appears later through status, events, logs, or notifications.

Do not switch a command from synchronous to asynchronous without reviewing user experience, transactions, retries, idempotency, and failure reporting. If a browser form previously showed validation errors immediately, queueing the command may make those errors appear too late.

Commands sent to queues must be serializable according to the queue system's rules. Avoid putting database connections, service objects, closures, or full ORM objects into commands. Prefer IDs and simple value data that the handler can use to reload current state.

Command Bus Middleware

A command bus can run middleware around handlers. Middleware might open a transaction, validate commands, log duration, enforce authorization, add tracing, or dispatch commands to a queue. This can be useful when many handlers need the same cross-cutting behavior.

Middleware order matters. Validation should usually happen before opening expensive work. Authorization may need loaded data, so it may belong inside the handler or a handler-specific policy rather than generic middleware. Transaction middleware should not wrap external calls that cannot roll back.

Do not add a command bus just to make code look architectural. If the project has only a few use cases and no shared middleware need, direct action or service classes may be clearer.

Results And Exceptions

Some command handlers return nothing because success is enough and failures throw exceptions. Others return result objects because business outcomes are expected and need explicit handling. Choose one convention deliberately.

For example, ChangeEmailHandler might return EmailChanged, EmailAlreadyUsed, or UserMissing outcomes. It might throw only for unexpected infrastructure failure. This makes controllers and jobs handle normal business cases without parsing exception messages.

Asynchronous command handlers usually cannot return a useful value to the original caller. They should record durable status, publish events, or update records so the system can observe the outcome.

Idempotency And Retries

Command handlers often run under retry policies, especially when queued. A retry after a timeout or worker crash can run the same command again. Handlers should define whether repeated commands are safe.

Some commands are naturally idempotent, such as setting a user's marketing preference to false. Others are not, such as charging a payment or sending a one-time coupon. Use idempotency keys, unique constraints, processed-message tables, provider references, or state checks to prevent duplicate side effects.

Retry middleware cannot make an unsafe handler safe. It can only retry according to the failure categories the handler and infrastructure expose.

Command Handler Versus Action

Actions and command handlers are close relatives. An action usually exposes a method called directly by a controller or job. A command handler receives a command object and may be routed by a bus. If a project does not need a bus, actions can be simpler.

Use command handlers when command objects provide clear value: queue serialization, middleware, audit logs, typed messages, or multiple delivery mechanisms. Use actions when a simple one-use-case class is enough.

Avoid wrapping every action in a command and every command in an action. Pick one use-case boundary style per area unless a real integration requires both.

Versioning Command Messages

Queued commands can outlive a deployment. A message created by the old code may be handled by new code minutes or hours later. That means command payloads need compatibility discipline. Prefer additive changes, defaults for newly optional fields, and clear handlers for older message shapes when queues may contain old messages.

Do not rename command fields casually if serialized messages are already waiting. If a breaking payload change is unavoidable, drain the queue, route old and new command types separately, or add a migration handler that can understand both shapes. This concern is less visible for synchronous handlers, but it matters immediately once commands cross a queue or message broker boundary.

Record the command schema expectation near the producer and handler so reviewers can spot incompatible deployment changes before messages are stranded.

Testing Command Handlers

Test handlers with command objects and fake dependencies. Cover success, missing data, permission denial, validation rejection, duplicate command, temporary dependency failure, and side-effect recording.

If a command bus is used, also test routing and middleware separately. A handler test can pass while the bus maps the command to the wrong handler. Middleware tests should prove transaction, logging, retry, and validation order where those behaviors matter.

Queued commands need worker-level tests or integration checks. Serialization, retry delay, dead-letter behavior, and idempotency can fail outside the handler's unit test.

Common Failure Modes

One failure mode is command objects that carry too much. A full HTTP request, ORM entity, service object, or closure inside a command makes serialization, testing, and retry behavior fragile.

Another failure mode is a command bus hiding control flow. If every method call becomes dispatch(new SomethingHappened()), developers may struggle to find the handler, transaction boundary, and result path. Tooling and naming must make routing discoverable.

A third failure mode is pretending asynchronous work is immediate. A controller that dispatches a queued command and then shows "complete" is lying unless the work is already done.

A fourth failure mode is generic patch commands that bypass domain rules. Commands should represent application intent.

Review Criteria

When reviewing a command handler design, check the command name, payload, handler dependencies, result convention, transaction boundary, and retry safety. Ask whether the command could be serialized if it needs to be queued. Ask where normal business outcomes are represented.

Check whether a bus adds value. Shared middleware, queue dispatch, and routing can justify it. If not, a direct action may be easier.

Check tests for command-to-handler mapping when a bus exists. A handler that is never routed correctly is dead code.

What To Check

Before moving on, make sure you can:

  • define a command as an intent message and a handler as the executor
  • keep framework requests, services, and ORM objects out of queued command payloads
  • decide when a command bus is useful
  • distinguish synchronous and asynchronous command contracts
  • define result and exception conventions
  • design handlers for retries and idempotency
  • test handlers, routing, middleware, and worker behavior at the right level

After this lesson, you should be able to decide whether a PHP use case should be an action, service, command handler, or queued message and keep the command boundary explicit enough for retries and operations.

Practice

Task: Model A Command And Handler

Design a command and handler for changing a user's email address.

Requirements

Name the command class, its fields, the handler class, dependencies, normal outcomes, and unexpected failures.

Check your work

The command should carry intent and input, not service objects or an HTTP request.

Show solution

Use ChangeEmailCommand with fields such as userId, newEmail, and requestedBy. The values should be IDs or value objects, not a framework request or ORM entity.

Use ChangeEmailHandler with dependencies such as UserRepository, EmailUniquenessChecker, EmailChangePolicy, and an event recorder or outbox. The handler loads the user, checks permission and uniqueness, applies the change, saves it, and records an event.

Normal outcomes may include changed, user missing, forbidden, invalid email, or email already used. Unexpected database failure or outbox failure should surface as infrastructure failure according to the project's exception convention.

Task: Review Command Bus Fit

A small app has three use cases and no queues. A developer proposes a command bus, middleware stack, and command classes for every controller button.

Review whether that is justified.

Requirements

Discuss shared middleware, routing discoverability, current complexity, and when to adopt the bus later.

Check your work

The answer should not add a command bus just for architectural style.

Show solution

The command bus is probably premature. With three use cases, no queue, and no shared middleware need, direct actions or application services are easier to read and debug. A bus would add routing indirection and conventions before the project benefits from them.

Adopt a bus later if several handlers need shared transaction, validation, logging, authorization, or retry middleware, or if commands must be dispatched asynchronously. At that point, the team should add routing tests and document handler discovery.

For now, create focused use-case classes with explicit dependencies and clear results. That keeps a later migration to commands possible without forcing every controller through a bus today.

Task: Test A Command Handler

A queued SendInvoiceCommand handler loads an invoice, renders a PDF, stores it, and sends a notification.

Plan tests for handler behavior and queued-worker concerns.

Requirements

Include success, missing invoice, render failure, duplicate command, storage failure, notification failure, serialization, and retry/dead-letter behavior.

Check your work

The answer should separate handler unit tests from worker integration checks.

Show solution

Handler tests can use fake invoice repository, PDF renderer, storage, notifier, and processed-command store. The success test should render, store, mark the invoice as sent or record a sent event, and notify once.

Missing invoice should become a permanent failure or no-op according to product rules. Render and storage failures should prevent notification. Notification failure should be categorized so retry policy is clear. A duplicate command should not create a second stored PDF or second notification when idempotency requires one result.

Worker integration checks should prove the command serializes correctly, retries temporary failures with limits, and reaches a dead-letter or failed-job path after exhaustion. These concerns are outside a pure handler unit test.