4 The honest hexagon: Use cases, commands, and the case against exceptions
The chapter explains how the application layer acts as the honest boundary between external adapters and the domain model. Use cases are defined as input ports: typed, domain-language interfaces that describe what the system can do without binding it to HTTP, messaging, or persistence details. Commands and queries are treated as self-validating input objects that are created only when valid, so handlers can focus on business orchestration rather than input cleanup or framework concerns.
A major theme is that a method returning void leaves the failure path unspecified, forcing callers to guess which exceptions may occur. The chapter argues that expected outcomes such as validation problems, missing data, authorization failures, or recoverable infrastructure issues should be part of the contract, not hidden in exceptions. To make this explicit, it introduces Either<Error, R>, with a sealed error hierarchy that distinguishes business, validation, and technical contingencies, enabling compiler-visible failure handling, better tests, and clearer control flow.
The chapter then shows how handlers implement use cases by composing repositories, domain methods, and output ports through Either chains, while staying free of business rules themselves. It extends this with command and query buses, which let adapters route requests through a single entry point instead of depending on many specific handlers, improving flexibility and reducing coupling. Finally, it introduces domain events as a way to keep use cases focused while allowing secondary reactions—such as notifications, analytics, or statistics updates—to happen through separate listeners, preserving single responsibility and preparing the ground for a fuller event-driven story later.
An input port at the hexagon boundary. The REST controller depends on the interface, never on the concrete handler. Dependency flows inward; the handler is invisible to the outside world.
The sealed Error hierarchy for WriteFlow. BusinessError and TechnicalError are nested sealed interfaces with their own record subtypes. ValidationErrors carries a list of ValidationError instances, which live in a separate sealed hierarchy outside Error. MultipleErrors allows composing multiple errors from different categories into a single failure.
Command bus routing. The controller sends a command to the bus. The bus looks up the handler through a registry and delegates execution. The controller never knows which handler processed the command.
Decorator chain. The adapter calls a single CommandBus interface. Behind it, decorators wrap the real bus, each adding one concern. The handler at the end is resolved through the registry and is invisible to the adapter. Adding or removing decorators requires no changes to handlers or adapters.
The domain event flow. The aggregate generates the event. The handler publishes it after the save. An input adapter listens and invokes a new use case. The event exits the hexagon through an output port and re-enters through an input adapter—maintaining the architectural boundary in both directions.
Summary
- Input ports are interfaces that define what the application can do, expressed in domain language. They are the hexagon's public API—the only surface adapters depend on.
- The void return type on a command is correct in intent—commands do not return data. But combined with exceptions as the sole error channel, it leaves the failure path invisible to the compiler, to callers, and to tests.
- Either<Error, Void> completes the contract without violating CQS. The right side remains Void—no data returned. The left side carries a typed error. Both outcomes are explicit, and both are compiler-enforced.
- The Error sealed hierarchy distinguishes between BusinessError (domain rule violations), TechnicalError (infrastructure failures), and ValidationErrors (malformed input carrying a list of ValidationError instances). Each category maps to distinct adapter responses.
- Commands and queries are typed, self-validating input data carriers. They implement marker interfaces (Command, Query) from the Architecture Kernel and use the validateThenCreate factory method to guarantee validity at construction—never throwing, always returning Either.
- Application handlers orchestrate without owning business logic. They coordinate repositories and output ports, delegate business decisions to the domain model, and return Either. Cross-cutting concerns may attach through technology-agnostic annotations from the Architecture Kernel.
- The command bus and query bus decouple adapters from handlers. The adapter injects one interface—CommandBus or QueryBus—and the bus resolves the correct handler at runtime through a registry. Adding a new use case means adding a new handler class—no adapter changes, no wiring updates.
- In WriteFlow, the application skeleton now has one write slice and one read slice: publishing an article through PublishArticleCommand and retrieving an article through GetArticleByIdQuery. Both enter through validated input objects, flow through a bus, reach a handler, and return explicit Either-based outcomes.
- Decorators wrap the bus to add technical cross-cutting behavior—logging, metrics, tracing, validation, or security propagation—without modifying handlers or adapters. They compose into chains controlled at assembly time and are different from domain events, which model domain-specific consequences of state changes.
- Domain events separate primary tasks from secondary reactions. The aggregate generates the event, the handler publishes it after persistence, and input adapters listen and invoke additional use cases. Each secondary reaction lives in its own class, independently addable and removable.
FAQ
1. What is a use case in hexagonal architecture?
A use case is an application-layer capability that coordinates work between external actors and the domain model. It exposes an input port contract in domain language, such as publishing an article or retrieving a feed, without containing business logic itself.2. Why are input ports defined as Java interfaces?
Input ports are Java interfaces because they define the application’s public API independently of any delivery mechanism. Controllers, message consumers, and scheduled jobs depend on the interface, while the implementation stays hidden inside the hexagon.3. Why should commands be self-validating before reaching a handler?
Commands should be validated at creation time so the handler can trust its input and focus only on business orchestration. This keeps invalid data out of the application layer and shifts responsibility for data hygiene to the boundary.4. What is wrong with a void handle(SomeCommand command) signature?
It tells callers that no data is returned, but it says nothing about failure. If something goes wrong, the only path is an exception, which is invisible to the compiler and forces callers to guess what might happen.5. Why are exceptions a poor sole error channel for expected failures?
Expected failures like missing data, authorization problems, or validation errors are normal outcomes, not exceptional faults. Modeling them with exceptions hides the contract, makes testing awkward, and can express only one failure at a time.6. How does Either<Error, R> improve use case contracts?
Either makes success and failure explicit in the method signature. The left side carries a typed error, the right side carries the result, and the compiler forces callers to acknowledge both branches.7. Why does the book use a sealed Error hierarchy?
A sealed hierarchy gives a closed, compiler-known set of failure types. This lets the application distinguish between validation, business, and technical failures instead of collapsing everything into one generic error.8. What is the difference between commands and queries?
Commands change state and returnVoid on success, while queries retrieve data and return a result DTO or projection. Both are typed, validated inputs, but they serve different purposes and should use different handlers.
Clean Applications with Hexagonal Architecture ebook for free