Command Handlers
When a command arrives, something needs to orchestrate the response: load the right aggregate, invoke the domain method, persist the result, and handle errors. If this orchestration lived in the API controller, domain logic would leak into infrastructure code. If it lived in the aggregate, the aggregate would need to know about persistence.
Command handlers solve this by acting as thin orchestrators — they receive a command, coordinate the necessary steps, and delegate all business logic to the aggregate. The handler itself contains no business rules.
Facts
Command handlers are connected to an aggregate.
Command handlers are always connected to a single aggregate. One command handler per aggregate is the norm, with all aggregate commands handled within it.
A Command handler contains multiple handlers.
Each method in a command handler is connected to a specific command to handle and process.
Handlers are single-purpose.
Each handler method is responsible for handling a single type of command. This ensures that the command handling logic is focused and manageable.
Handlers should deal only with the associated aggregate.
Methods in a command handler should only deal with managing the lifecycle of the aggregate associated with it. Any state change beyond an aggregate's boundary should be performed by eventual consistency mechanisms, like raising an event and consuming it in the event handler of the other aggregate.
Command handlers invoke domain logic.
Command handlers do not contain business logic themselves. Instead, they invoke methods on aggregates or domain services to perform the necessary actions.
Command handlers coordinate actions.
Command handlers coordinate multiple actions related to each command. Primarily, this involves hydrating (fetching) the aggregate, invoking methods to perform state changes and persisting changes through a repository.
Command handlers can return values.
When processed synchronously, command handlers can return values to the caller. This is useful for scenarios like authentication where you need to return a token, or when you need to return the ID of a newly created resource.
Unlike event handlers (which can have multiple handlers for a single event and don't return values), a command can only be handled by a single handler, allowing the return value to be passed back to the caller. This is consistent with the command pattern, where a command represents an intent to perform an action and may need to provide immediate feedback.
By default, command handlers return the position of the command in the command stream back to the caller.
Handler methods are enclosed in Unit of Work context.
Each handler method is automatically enclosed within a Unit of Work context. This means that all interactions with the infrastructure is packaged into a single transaction. This makes it all the more important to not mix multiple responsibilities or aggregates when handling a command.
Commands can be handled asynchronously.
While handling commands synchronously is the norm to preserve data integrity, it is possible to configure the domain to handle commands asynchronously for performance reasons.
Best Practices
Ensure idempotency.
Command handling should be idempotent, meaning that handling the same command multiple times should not produce unintended side effects. This can be achieved by checking the current state before applying changes.
Handle exceptions gracefully.
Command handlers should handle exceptions gracefully, ensuring that any necessary rollback actions are performed and that meaningful error messages are returned to the caller.
Validate commands.
Ensure that commands are validated before processing. This can be done in a separate validation layer or within the command handler itself.
Return values only when necessary.
Only return values from command handlers when they are genuinely needed by the caller. For example, return authentication tokens or newly created resource IDs, but not entire entity representations. Return values should be small and relevant to the immediate needs of the caller.
Next steps
For practical details on defining and using command handlers in Protean, see the guide:
- Command Handlers — Defining handlers, workflow, return values, idempotency, and error handling.
For the read-side counterpart:
- Query Handlers — Processing structured read intents from projections (the read side of CQRS).
For design guidance:
- Application Service vs Command Handler — When to use which, with decision tree and comparison table.
- Thin Handlers, Rich Domain — Keeping handlers thin by pushing logic into the domain model.
- Command Idempotency — Handling duplicate commands safely.