Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

hexagonal architecture #39

Merged
merged 10 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ dist
# TernJS port file
.tern-port

# VS code internal folder
.vscode
# Stores VSCode versions used for testing VSCode extensions
.vscode-test

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
** xref:getting_started/create_rest_service.adoc[]
* Architecture
** xref:architecture/layered_architecture.adoc[]
** xref:architecture/hexagonale_architecture.adoc[]

* Integration
** xref:integration/rest_service.adoc[]
Expand Down
218 changes: 218 additions & 0 deletions modules/ROOT/pages/architecture/hexagonale_architecture.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
:imagesdir: ../images
// Open Questions/Discussion:
// - Is everything considered in the structure or are there things we cannot handle currently?
// - What about shared technical elements? (AOP for logging across multiple rest adapters)
// - How to organize multiple modules in a modular monolith and what about shared stuff there?
// - Should we really name the adapters after the protocol or more speaking (e.g. reservation for a inbound adapter for reservation)
// - Is structuring core and adapters in packages enough or should we use gradle modules or java modules?

// Todos:
// - Create ADR (Architecture Design records)
// - Mention that the structure can be made more lean at any time. Consider three case:
// - Complex application with great logic (Probably JPA and entities different, also a great core layer)
// - Application containing logic, but entities and JPA do not differ (potentially a case where the entities in Core can contain JPA specific annotations t avoid mapping)
// - Pure CRUD application (Leave out core)
// - Add an ArchUnit example to make call options explicit for devs (and reusable)
= Hexagonal Architecture

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.
The core contains the business logic and communicates with the external world via well-defined ports (interfaces).
Adapters implement these ports and handle the translation between the core's domain model and the specific technologies or protocols used by external systems.

[[img-t-hexagonal-architecture]]
.Hexagonal Architecture Reference
image::hexagonal_component_architecture_overview.drawio.svg["devonfw hexagonal architecture blueprint",scaledwidth="80%",align="center"]
// ----
//Created the directory tree based on this list using https://tree.nathanfriend.io/
// 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:

// - application
// - config
// - Configuration.java
// - core
// - domain
// - Customer.java
// - CustomerFactory.java
// - Reservation.java
// - Table.java
// - ports
// - in
// - AddReservationPort.java
// - CancelReservationPort.java
// - AlterReservationPort.java
// - AddTablePort.java
// - RemoveTablePort.java
// - out
// - StoreReservationPort.java
// - usecase
// - AddReservationUc.java
// - ManageReservationUc.java
// - AddTableUc.java
// - [service]
// - [FindFreeTableService.java]
// - adapter
// - in
// - rest
// - RestController.java
// - model
// - ReservationDto.java
// - mapper
// - ReservationMapper.java
// - out
// - jpa
// - JpaAdapter.java
// - model
// - ReservationEntity.java
// - TableEntity.java
// - mapper
// - ReservationJpaMapper.java
// - TableJpaMapper.java
// ----

[source,plaintext]
----
.
└── application/
├── config/
│ └── Configuration.java
│ └── Configuration.java
├── core/
│ ├── domain/
│ │ ├── Customer.java
│ │ ├── CustomerFactory.java
│ │ ├── Reservation.java
│ │ └── Table.java
│ ├── ports/
│ │ ├── in/
│ │ │ ├── AddReservationPort.java
│ │ │ ├── CancelReservationPort.java
│ │ │ ├── AlterReservationPort.java
│ │ │ ├── AddTablePort.java
│ │ │ └── RemoveTablePort.java
│ │ └── out/
│ │ └── StoreReservationPort.java
│ ├── usecase/
│ │ ├── AddReservationUc.java
│ │ ├── ManageReservationUc.java
│ │ └── AddTableUc.java
│ └── [service]/
│ └── [FindFreeTableService.java]
└── adapter/
├── in/
│ └── rest/
│ ├── RestController.java
│ ├── model/
│ │ └── ReservationDto.java
│ └── mapper/
│ └── ReservationMapper.java
└── out/
└── jpa/
├── JpaAdapter.java
├── model/
│ ├── ReservationEntity.java
│ └── TableEntity.java
└── mapper/
├── ReservationJpaMapper.java
└── TableJpaMapper.java
----
[cols="20,~", options="header"]
|===
| Package | Description

| core
| 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.

| core.domain
| The domain circle contains the entities and value objects of the business domain of the application.
Related Factories or Builders are located here as well.
It's proposed to make entities anemic. See <<_anemic_vs_rich_domain_models>>

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

| core.port
| Ports are interfaces, that are used by the core and should be implemented by an according adapter.
Ports should not be technology specific.
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.
It needs to be distinguished between inbound ports and outbound ports.

| core.port.in
| Inbound ports are the entry of the application.
They provide interfaces that are called from inbound adapters and hide the actual implementation.
A proposal of structuring inbound ports is naming them like single use cases (e.g. CancelReservationPort).
Each port should only provide a single method.
.Design Decision
[%collapsible]
====
Inbound Ports are not as relevant for the hexagonal architecture as the outbound ports.
Outbound ports are used for the dependency inversion pattern.
For inbound ports could also call the use cases directly.
Therefore, an pragmatic alternative would be leaving out the inbound ports.

It was decided to include the inbound ports nonetheless. They should implement single use cases that are offered.
Each interface should clearly mark the use case that contains only one method.
Use cases from the interface might be grouped logically in the use case implementation class.
====

| core.port.out
| Outbound ports decouple the interaction with external systems.
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.
Outbound ports should describe the business need for the communication (e.g. StoreReservationPort). How this is then realized depends on the adapter that implements it.
This way a technology can be easily replaced.
For example storing the reservation could be be realized in a first prototype by writing the objects to a file.
Later it could be replaced with a database.
The core logic would be untouched by that.

| [optional] core.service
| Services can be considered as business helper classes.
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.
Services are optional as they can be used, when there's a real need. Usually a use case should contain the business logic.

| adapter
a| Adapters connect the application core to the surrounding context. They have the following tasks:

* Implement a specific protocol to connect to the context. E.g REST, JDBC, MQTT, ...
* Maintain a data model that is necessary to communicate with the context
* Translate the domain model from the core to that model or vice versa
* Handle protocol specific errors
* Log the interaction with the surrounding context

| adapter.in
| Inbound adapters specify connection points for everything that can trigger the business logic.
That might be interfaces (HTML, RPC, etc), Message Consumers or schedulers for batch processing.
Inside the adapters further packages are differentiating the protocols that is used (e.g. `.rest`).

| adapter.out
| Outbound adapters define outgoing connections where the application actively interacts with context outside.
That can be database connections, file operations, API calls, message producing and many more.
Inside the adapters further packages are differentiating the protocols that is used (e.g. `.jpa`).
|===


== Anemic vs Rich domain models
====
"In a rich domain model, as much of the domain logic as possible is implemented within the entities at the core of the application.
The entities provide methods to change state and only allow changes that are valid according to the business rules. [...]
In an “anemic” domain model, the entities themselves are very thin.
They usually only provide fields to hold." <<Hombergs21>>
====

Considering java as an object oriented language it feels natural to implement business logic inside the entities themselves.
In large scale application with mixed skilled people it's proposed to not use rich domain models.
There are two reasons for this.
First of all, the domain objects are returned to the adapters.
If they include business logic this is revealed and available outside of the core, which should not be the case.
The answer to this problem could be an additional mapping, but this leads to a lot of unpractical mappings.
Furthermore, adding the business logic to the domain entities spreads it across use cases, entities and services.
This makes the application more difficult to understand and harder to locate the place for new features or changes.

It's proposed to implement the domain model as anemic entities using Java records.
Use use cases and services to implement the business logic and interact with the domain models.


[bibliography]
== Bibliography
* [[[Hombergs21]]] Tom Hombergs. _Get Your Hands Dirty on Clean Architecture._ 2021.