5 Crossing the boundaries: Adapters, repositories, and the mapping problem
This chapter shifts from the inner workings of the application core to the practical problem of letting it communicate with the outside world. It shows how input adapters receive requests from mechanisms like HTTP, queues, or the CLI, translate them into commands or queries for the bus, and map results back into the format expected by the caller. On the other side, output adapters implement core-owned ports so the application can persist data, send notifications, and call external services without the core depending on infrastructure details.
A major theme is how boundaries should be designed so the core stays protected. Input adapters handle protocol-specific concerns such as translating the application’s error hierarchy into HTTP responses, while output adapters keep infrastructure concerns hidden behind role interfaces rather than header interfaces. The chapter also emphasizes that repositories are a special kind of output port: unlike other adapters, they are allowed to work directly with domain entities because their job is to persist and reconstitute domain state. At the same time, repository interfaces should still be carefully designed with separate read and write concerns, and framework-specific tools like Spring Data should be wrapped inside adapters rather than exposed to the core.
The final part examines the mapping problem in depth and treats mapping as a necessary “boundary tax” that buys isolation. It contrasts several strategies, from using the same type everywhere to a full bridge where every boundary has its own model and mapper. The chapter argues that while fewer mappings may feel simpler, they create dangerous coupling between API contracts, domain logic, and persistence schemas. The preferred approach for most systems is two-way mapping, while the full bridge is best for larger, longer-lived, or public systems where maximal isolation matters most.
This chapter completes the outside ring of the hexagon. Input adapters translate external requests into commands and queries, output adapters implement the ports required by the core, and explicit mapping keeps each boundary separate.
Two input adapters, one use case. The REST controller and the message consumer both call the same command bus. The handler behind the bus is identical regardless of which adapter triggered it.
Error hierarchy to HTTP status code mapping. Each sealed subtype maps to a specific status code. The compiler enforces completeness—no subtype can be silently ignored.
The header interface on the left is shaped by the email implementation. The role interface on the right is shaped by what the application core needs, keeping the port technology-agnostic.
Spring Data reinterpretation. The domain-owned ArticleRepository at the top (annotated with the Shared Kernel's @Repository) defines the contract. The adapter in the middle implements that port and delegates to Spring Data at the bottom (annotated with Spring's @Repository). Two different annotations with the same name, two different layers, two different purposes.
The Full Bridge for a publication-readiness query. The request enters as raw HTTP input, becomes an application query, resolves to a validated domain identifier, loads the Article aggregate because the answer depends on domain rules, maps the domain result to a DTO, and finally maps that DTO to an HTTP response. No type crosses more than one boundary.
Summary
- Input adapters are translators that receive external stimuli—HTTP requests, messages, scheduled triggers—and convert them into commands or queries the bus understands. They map the Error hierarchy to protocol-specific responses using exhaustive switch on sealed types. Framework annotations belong here, not in the core.
- Output adapters implement port interfaces defined by the core, using whatever technology is appropriate. They translate between the port's domain-language DTOs and the infrastructure's native format. The core never sees the adapter—only the port.
- Role interfaces vs header interfaces determine whether your ports protect the core or leak the infrastructure. A role interface is shaped by what the consumer needs. A header interface mirrors the implementation. Always design ports from the inside out.
- DTOs are the currency of output port boundaries—lightweight, immutable records that carry exactly the data the port needs. Domain entities never cross output port boundaries directly. The one exception is repositories.
- Repositories are special output ports that work directly with domain entities. Their purpose—persisting and reconstituting domain state—requires the domain model to cross the boundary. The repository interface lives in the core, annotated with the Shared Kernel's @Repository. The implementation lives in the adapter layer, annotated with @Adapter.
- Repository interfaces separate read-side from write-side through naming conventions: save and delete for writes, read for loading domain entities into command handlers, and find for returning DTOs to query handlers. Granularity beyond this—slicing into multiple interfaces—is a team decision, not an architectural one.
- Spring Data belongs inside the adapter, never on domain-owned ports. The SpringArticleRepository extends ListCrudRepository operating on a persistence entity, hidden behind the JpaArticleRepository adapter that the core depends on.
- Two @Repository annotations with the same name, two different layers, two different purposes.
- In WriteFlow, these boundaries complete the outside ring of the hexagon: REST controllers drive article use cases, repository adapters persist and reload articles, notification adapters deliver publication-related messages, and mappers keep each external representation separate from the domain model.
- The mapping problem arises at every boundary crossing. Four strategies exist along the convenience-isolation spectrum: no mapping (anti-pattern), one-way (API decoupled), two-way (API and persistence decoupled), and the Full Bridge (complete isolation at every boundary). Two-way is the pragmatic default. The Full Bridge is the right choice for public APIs, large teams, and long-lived systems. The only incorrect choice is no mapping at all.
Clean Applications with Hexagonal Architecture ebook for free