5 Crossing the boundaries: Adapters, repositories, and the mapping problem
The chapter explains how the application core, which already contains well-defined use cases, commands, queries, and handlers, connects cleanly to the outside world. It introduces the outer ring of the hexagon: input adapters that receive external requests and turn them into bus-friendly commands or queries, and output adapters that implement the core’s ports while hiding frameworks and delivery mechanisms. The main idea is that adapters are translators, not places for business logic, and that the core should remain isolated even as the system becomes usable through HTTP, messaging, or other interfaces.
A major focus is how to keep boundaries explicit when data crosses them. On the input side, adapters map protocol-specific requests into application inputs and convert typed application errors into protocol-specific responses, such as HTTP problem details. On the output side, adapters implement role interfaces shaped by the core’s needs rather than by infrastructure APIs, and they translate between domain language and technical concerns. The chapter stresses that DTOs should usually be the currency at boundaries, preventing domain entities from leaking into controllers, APIs, or external service calls.
Repositories get special treatment because they are output ports that legitimately work with domain entities: they load, persist, and reconstitute aggregates so handlers can invoke domain behavior on real objects. The chapter recommends separating read-side and write-side repository methods, keeping persistence frameworks like Spring Data hidden inside adapters, and using explicit mappers to move between domain entities, persistence models, DTOs, requests, and responses. It then compares four mapping strategies, from no mapping at all to the fully isolated “Full Bridge,” concluding that more mapping usually buys better decoupling, with the most isolated approach best suited to larger, longer-lived, or public systems.
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