Skip to content

Commit f18fdad

Browse files
committed
feat: Add intro to hexagonal architecture
1 parent d62f673 commit f18fdad

File tree

5 files changed

+148
-89
lines changed

5 files changed

+148
-89
lines changed

modules/ROOT/images/hexagonal_architecture_data_direction.svg

Lines changed: 4 additions & 0 deletions
Loading

modules/ROOT/images/hexagonal_architecture_example.svg

Lines changed: 4 additions & 0 deletions
Loading

modules/ROOT/images/hexagonal_architecture_idea.svg

Lines changed: 4 additions & 0 deletions
Loading

modules/ROOT/images/hexagonal_architecture_motivation.svg

Lines changed: 4 additions & 0 deletions
Loading

modules/ROOT/pages/architecture/hexagonal_architecture.adoc

Lines changed: 132 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,107 @@
11
:imagesdir: ../images
22
= Hexagonal Architecture
33

4-
Hexagonal architecture, also known as Ports and Adapters, is a software design pattern that promotes separation of concerns by organizing an application into a central core surrounded by external adapters.
5-
The core contains the business logic and communicates with the external world via well-defined ports (interfaces).
6-
Adapters implement these ports and handle the translation between the core's domain model and the specific technologies or protocols used by external systems.
4+
In many software systems, core business logic is entangled with technical concerns like persistence, messaging, and protocol handling. This coupling makes systems fragile, difficult to adapt, and hard to test.
75

8-
[[img-t-hexagonal-architecture]]
9-
.Hexagonal Architecture Reference
10-
image::hexagonal_component_architecture_overview.drawio.svg["devonfw hexagonal architecture blueprint",scaledwidth="80%",align="center"]
6+
.Infrastructural Entanglement
7+
image::hexagonal_architecture_motivation.svg[align="center"]
8+
9+
As illustrated in Figure 1 (_Infrastructural Entanglement_), application logic is directly tied to external systems — databases, user interfaces, messaging infrastructure — via concrete implementations throughout the application. Domain models are aware of their environment and dependencies are pervasive: HTTP controllers call service methods that operate on ORM-managed entities; repositories return framework-specific types; and cross-cutting concerns such as error handling, logging, and authentication are scattered throughout the codebase.
10+
11+
This tight coupling leads to several recurring challenges:
12+
13+
- *Hard to adapt:* Infrastructure changes (e.g., migrating from a relational database to NoSQL, or shifting from REST to asynchronous messaging) require invasive changes to business logic, even if functional requirements stay the same.
14+
- *Difficult to test:* Business logic is hard to isolate for unit testing due to deep technical dependencies. Tests either require real external systems or rely on brittle mocking setups.
15+
- *Blurred concerns:* Business logic is mixed with infrastructure logic — transaction management, logging, exception mapping — making it harder to understand, modify, or reuse.
16+
17+
While traditional layered architectures (e.g., presentation → service → persistence) attempt to organize these concerns, they often still expose the domain layer to infrastructure details. In contrast, hexagonal architecture addresses these challenges through more structural response that utilizes the dependency inversion principle.
18+
19+
== Hexagonal Blueprint
20+
21+
Hexagonal Architecture — also known as the _Ports and Adapters_ pattern — is an architectural style that centers the domain model, surrounded by abstract interfaces to the outside world. These interfaces, called *ports*, act like sockets through which external components interact with the core. Concrete implementations — known as *adapters* — connect to these ports and handle the specifics of communication, persistence, or protocol integration.
22+
23+
This design follows the *Dependency Inversion Principle*: the domain depends only on abstractions, not specific technologies. It neither knows nor cares what framework, database, or transport mechanism is in use — only that the contractual behavior of the port is respected.
24+
25+
Adapter wiring is delegated to the runtime environment, often via a dependency injection framework such as Spring or Quarkus. This allows the core to remain agnostic of instantiation logic and independent of its execution environment.
26+
27+
.Hexagonal Architecture in Design
28+
image::hexagonal_architecture_idea.svg[align="center"]
29+
30+
The resulting structure is shown in Figure 2 (_Hexagonal Architecture in Design_): the business logic resides at the core, surrounded by inbound and outbound ports. Adapters implement these ports and handle concerns such as user input, persistence, messaging, or observability — without leaking into the domain layer.
31+
32+
== Practical Example
33+
34+
To make this structure concrete, consider a system where two application use cases — reservation and batch processing — form the core of the business logic.
35+
36+
.Hexagonal Architecture in Practice
37+
image::hexagonal_architecture_example.svg[align="center"]
38+
39+
This setup is shown in Figure 3 (_Hexagonal Architecture in Practice_). Both use cases are implemented inside the core and exposed through dedicated interfaces: `ReservationPort` and `BatchPort`. These interfaces form the system’s inbound ports — stable, technology-agnostic entry points into the domain.
40+
41+
External inputs are handled by inbound adapters. A `RestController` invokes `ReservationPort` to process HTTP requests. The `GrpcAdapter` implements `BatchPort`, allowing background tasks or other systems to initiate batch processing via gRPC. These adapters translate protocols and delegate to the core without introducing infrastructure-specific concerns into the domain.
42+
43+
The domain, in turn, depends on two outbound ports: `PersistencePort` and `PaymentPort`. These define how the domain interacts with data stores and external services. Their implementations — `JpaAdapter` and `SoapAdapter`, respectively — encapsulate technical details such as database access or remote service communication.
44+
45+
Because the core depends only on ports, it stays isolated from the specifics of HTTP, gRPC, JPA, or SOAP. Replacing an adapter — for example, swapping SOAP for a REST-based payment gateway — does not require any changes to the domain layer. This separation keeps infrastructure decisions localized and reversible while ensuring that the core remains stable and testable.
46+
47+
== Dependency Direction and Data Ownership
48+
49+
Hexagonal Architecture enforces a strict rule: dependencies point inward. Adapters may depend on the domain, but the domain must not depend on adapter code — not even indirectly through return types. This constraint has important implications for how data flows across layers.
50+
51+
.Zooming into the Persistence Port
52+
image::hexagonal_architecture_data_direction.svg[align="center"]
53+
54+
This is illustrated in Figure 4 (_Zooming into the Persistence Port_). For example, when the domain calls `PersistencePort` and expects a return value, the adapter must ensure that no infrastructure-specific types leak into the core. Returning a `ReservationEntity` — a JPA class defined in the adapter — would violate this rule by making the domain aware of persistence details.
55+
56+
There are two primary strategies to preserve the direction of dependency:
57+
58+
- *Explicit Mapping Inside the Adapter:*
59+
The adapter maps domain objects to persistence entities for storage, and maps them back before returning results. This approach keeps the domain clean and separate, though it requires boilerplate mapping logic.
60+
61+
- *Domain Interfaces Implemented by the Adapter:*
62+
Alternatively, the domain model can be defined as interfaces (e.g. `Reservation`). The adapter implements these interfaces in its entity classes. This allows the adapter to return its own types while keeping the domain dependent only on abstractions.
63+
64+
Each strategy has trade-offs. Mapping provides clear boundaries and flexibility but increases code volume. Interface-based design reduces duplication but may blur the separation between layers. The choice depends on the complexity of the domain and how much semantic weight the model carries.
65+
66+
Regardless of approach, the architectural constraint holds: data flows inward, and dependencies never reverse. The domain defines what it needs; the adapter provides it — without exposing implementation details or violating the system’s structural integrity.
67+
68+
=== Package Structure
69+
The following table shows a package structure that fits the hexagonal design and focuses on the reservation use case of the application:
1170
// ----
1271
//Created the directory tree based on this list using https://tree.nathanfriend.io/
1372
// As the list is easier to maintain, try to do edits in the list structure, use the tool mentioned above and paste both in here:
1473

15-
// - application
16-
// - core
17-
// - domain
18-
// - Customer.java
19-
// - CustomerFactory.java
20-
// - Reservation.java
21-
// - Table.java
22-
// - ports
23-
// - in
24-
// - AddReservationPort.java
25-
// - CancelReservationPort.java
26-
// - AlterReservationPort.java
27-
// - AddTablePort.java
28-
// - RemoveTablePort.java
29-
// - out
30-
// - StoreReservationPort.java
31-
// - usecase
32-
// - AddReservationUc.java
33-
// - ManageReservationUc.java
34-
// - AddTableUc.java
35-
// - [service]
36-
// - [FindFreeTableService.java]
37-
// - adapter
38-
// - in
39-
// - web
40-
// - RestController.java
41-
// - model
42-
// - ReservationDto.java
43-
// - mapper
44-
// - ReservationMapper.java
45-
// - out
46-
// - repository
47-
// - JpaAdapter.java
48-
// - model
49-
// - ReservationEntity.java
50-
// - TableEntity.java
51-
// - mapper
52-
// - ReservationJpaMapper.java
53-
// - TableJpaMapper.java
74+
// application
75+
// core
76+
// domain
77+
// Reservation.java
78+
// ports
79+
// in
80+
// ReservationPort.java
81+
// out
82+
// PersistencePort.java
83+
// usecase
84+
// AddReservationUseCase.java
85+
// CancelReservationUseCase.java
86+
// PostponeReservationUseCase.java
87+
// service
88+
// ReservationService.java
89+
// adapter
90+
// in
91+
// web
92+
// RestController.java
93+
// model
94+
// ReservationDto.java
95+
// mapper
96+
// ReservationMapper.java
97+
// out
98+
// persistence
99+
// JpaAdapter.java
100+
// model
101+
// ReservationEntity.java
102+
// logic
103+
// ReservationRepository.java
104+
// ReservationMapper.java
54105
// ----
55106

56107
[source,plaintext]
@@ -59,25 +110,18 @@ image::hexagonal_component_architecture_overview.drawio.svg["devonfw hexagonal a
59110
└── application/
60111
├── core/
61112
│ ├── domain/
62-
│ │ ├── Customer.java
63-
│ │ ├── CustomerFactory.java
64-
│ │ ├── Reservation.java
65-
│ │ └── Table.java
113+
│ │ └── Reservation.java
66114
│ ├── ports/
67115
│ │ ├── in/
68-
│ │ │ ├── AddReservationPort.java
69-
│ │ │ ├── CancelReservationPort.java
70-
│ │ │ ├── AlterReservationPort.java
71-
│ │ │ ├── AddTablePort.java
72-
│ │ │ └── RemoveTablePort.java
116+
│ │ │ └── ReservationPort.java
73117
│ │ └── out/
74-
│ │ └── StoreReservationPort.java
118+
│ │ └── PersistencePort.java
75119
│ ├── usecase/
76-
│ │ ├── AddReservationUc.java
77-
│ │ ├── ManageReservationUc.java
78-
│ │ └── AddTableUc.java
79-
│ └── [service]/
80-
│ └── [FindFreeTableService.java]
120+
│ │ ├── AddReservationUseCase.java
121+
│ │ ├── CancelReservationUseCase.java
122+
│ │ └── PostponeReservationUseCase.java
123+
│ └── service/
124+
│ └── ReservationService.java
81125
└── adapter/
82126
├── in/
83127
│ └── web/
@@ -87,14 +131,13 @@ image::hexagonal_component_architecture_overview.drawio.svg["devonfw hexagonal a
87131
│ └── mapper/
88132
│ └── ReservationMapper.java
89133
└── out/
90-
└── repository/
134+
└── persistence/
91135
├── JpaAdapter.java
92136
├── model/
93-
│ ├── ReservationEntity.java
94-
│ └── TableEntity.java
95-
└── mapper/
96-
├── ReservationJpaMapper.java
97-
└── TableJpaMapper.java
137+
│ └── ReservationEntity.java
138+
└── logic/
139+
├── ReservationRepository.java
140+
└── ReservationMapper.java
98141
----
99142
[cols="20,~", options="header"]
100143
|===
@@ -104,54 +147,54 @@ image::hexagonal_component_architecture_overview.drawio.svg["devonfw hexagonal a
104147
| The core contains the essential business logic, domain entities, and use cases. It focuses on implementing the main functionalities while remaining technology-agnostic. The core interacts with external components through well-defined interfaces called "ports," ensuring a clear separation of concerns and promoting flexibility, testability, and maintainability.
105148

106149
| core.domain
107-
| The domain package contains the entities and value objects of the business domain of the application.
108-
Related Factories or Builders are located here as well.
150+
| The domain package contains the entities and value objects of the business domain of the application.
151+
Related Factories or Builders are located here as well.
109152
It's proposed to make entities anemic. See <<_anemic_vs_rich_domain_models>>
110153

111154
| core.usecase
112-
| Use Cases are the main entrypoint of the applications core.
113-
They validate the given input and orchestrate the domain entities, services and ports to implement a Business Use Case.
114-
Usually a use case implementation should only include a small dedicated use case.
155+
| Use Cases are the main entrypoint of the applications core.
156+
They validate the given input and orchestrate the domain entities, services and ports to implement a Business Use Case.
157+
Usually a use case implementation should only include a small dedicated use case.
115158
Depending of the size and adjacency of the use cases a grouping might make sense (e.g. ManageTableUc)
116159

117160
| core.port
118-
| Ports are interfaces, that are used by the core and should be implemented by an according adapter.
119-
Ports should not be technology specific.
120-
One big advantage of the hexagonal architecture is, that the adapters can be changed without changing the core and therefore, without touching the business logic.
161+
| Ports are interfaces, that are used by the core and should be implemented by an according adapter.
162+
Ports should not be technology specific.
163+
One big advantage of the hexagonal architecture is, that the adapters can be changed without changing the core and therefore, without touching the business logic.
121164
It needs to be distinguished between incoming ports and outgoing ports.
122165

123166
| core.port.in
124167
a| Incoming ports are the entry of the application.
125-
They provide interfaces that are called from incoming adapters and hide the actual implementation.
126-
A proposal of structuring incoming ports is naming them like single use cases (e.g. CancelReservationPort).
168+
They provide interfaces that are called from incoming adapters and hide the actual implementation.
169+
A proposal of structuring incoming ports is naming them like single use cases (e.g. CancelReservationPort).
127170
Each port should only provide a single method.
128171

129172
.Design Decision
130173
[%collapsible]
131174
====
132175
Incoming Ports are not as relevant for the hexagonal architecture as the outgoing ports.
133-
Outgoing ports are used for the dependency inversion pattern.
134-
For incoming ports could also call the use cases directly.
176+
Outgoing ports are used for the dependency inversion pattern.
177+
For incoming ports could also call the use cases directly.
135178
Therefore, an pragmatic alternative would be leaving out the incoming ports.
136179
137-
It was decided to include the incoming ports nonetheless. They should implement single use cases that are offered.
180+
It was decided to include the incoming ports nonetheless. They should implement single use cases that are offered.
138181
Each interface should clearly mark the use case that contains only one method.
139182
Use cases from the interface might be grouped logically in the use case implementation class.
140183
====
141184

142185
| core.port.out
143186
| Outgoing ports are an abstraction of everything in the surrounding context that is actively triggered by the core or used as data sink.
144187
This might include other services that are called, files that are written, databases, event streaming and everything the application is actively triggering outside of the core.
145-
Outgoing ports should describe the business need for the communication (e.g. StoreReservationPort). How this is then realized depends on the adapter that implements it.
146-
This way a technology can be easily replaced.
147-
For example storing the reservation could be be realized in a first prototype by writing the objects to a file.
188+
Outgoing ports should describe the business need for the communication (e.g. StoreReservationPort). How this is then realized depends on the adapter that implements it.
189+
This way a technology can be easily replaced.
190+
For example storing the reservation could be be realized in a first prototype by writing the objects to a file.
148191
Later it could be replaced with a database.
149192
The core logic would be untouched by that.
150193

151194
| [optional] core.service
152-
| Services can be considered as business helper classes.
195+
| Services can be considered as business helper classes.
153196
They provide a reusable part of the applications business logic that is used by multiple use cases or that helps to structure the application in a logical way.
154-
Services are optional as they can be used, when there's a real need.
197+
Services are optional as they can be used, when there's a real need.
155198
Usually a use case should contain the business logic.
156199

157200
| adapter
@@ -164,12 +207,12 @@ a| Adapters connect the application core to the surrounding context. They have t
164207
* Log the interaction with the surrounding context
165208

166209
| adapter.in
167-
| Incoming adapters specify connection points for everything that can trigger the business logic.
210+
| Incoming adapters specify connection points for everything that can trigger the business logic.
168211
That might be interfaces (HTML, RPC, etc), Message Consumers or schedulers for batch processing.
169-
Inside the adapters further packages are differentiating the category of the adapter (e.g. `.web`).
212+
Inside the adapters further packages are differentiating the category of the adapter (e.g. `.web`).
170213

171214
| adapter.out
172-
| Outgoing adapters define outgoing connections where the application actively interacts with context outside.
215+
| Outgoing adapters define outgoing connections where the application actively interacts with context outside.
173216
That can be database connections, file operations, API calls, message producing and many more.
174217
Inside the adapters further packages are differentiating the category of the adapter (e.g. `.repository`).
175218
|===
@@ -188,10 +231,10 @@ Considering java as an object oriented language it feels natural to implement bu
188231
In large scale application we propose to not use rich domain models.
189232
There are two reasons for this:
190233

191-
. the domain objects are returned to the adapters.
234+
. the domain objects are returned to the adapters.
192235
If they include business logic this is revealed and available outside of the core, which should not be the case.
193236
The answer to this problem could be an additional mapping, but this leads to a lot of unpractical mappings.
194-
. adding the business logic to the domain entities spreads it across use cases, entities and services.
237+
. adding the business logic to the domain entities spreads it across use cases, entities and services.
195238
This makes the application more difficult to understand and harder to locate the place for new features or changes.
196239

197240
Therefore, we propose to implement the domain model as anemic entities and make usage of use cases and services to implement the business logic and interact with the domain models.

0 commit comments

Comments
 (0)