Hexagonal Architecture (Part#1)

Adaptable and flexible software.

In the traditional Layered architecture, there is a web (UI) layer, a domain layer and a persistence layer.

In this architecture, everything builds on top of the persistence layer. The ORM based entity has to be in the persistence layer but needs to be used by domain layer. This creates a strong coupling between the Persistence layer and the Domain layer. Though this can be solved by Dependency Inversion but it still is not a good idea to make Persistence layer the foundation of our architecture.

Our Domain layer should know nothing about the Persistence or Web layer or their frameworks and should only concentrate on the business logic. Instead of database-driven, we want our design to be domain-driven.

Domain-driven clean architecture comes with a cost. Since the domain layer is completely decoupled from the outer layers, we have to maintain a model of our application’s entities in each of the layers.

Say, we are using object-relational mapping (ORM) framework in our persistence layer. An ORM framework expects specific entity classes that maps object fields to database columns. Since, domain layer does not know the persistence layer, we cannot use the same entity classes and have to create them in both the layers. This means we have to translate between both representations when the domain layer sends and receives data to and from the persistence layer (or any 2 layers for that matter).

Hexagonal architecture is a way to create domain-driven systems.

Hexagonal Architecture box diagram
Hexagonal Architecture Box diagram

Within the hexagon, we find our domain entities and the use-cases that work with them.

Outside the hexagon, we find various adapters that interact with the application. There might be web adapter that interacts with a web browser, some adapters interacting with external systems and an adapter that interacts with the database.

The adapters on the left are the adapters that driver our application by calling our application core. The adapters on the right are driven by our application because they are called by our application core.

To allow communication between the application core and the adapters, the application core provides specific ports. For driving adapters, such a port might be an interface that is implemented by one of the use-cases in the core and called by the adapter. For a driven adapter, it might be an interface that is implemented by the adapter and called by the core.

Hexagonal architecture is also known as “ports-and-adapters” architecture.

In greenfield software projects, the first thing we try to get right is the package structure. As the project grows, we realize that the package structure is just a nice-looking façade for an unstructured mess of code.

Lets discuss 3 options of structuring code for a payment application, specifically “Send Money” use case, with which a user can transfer money from their account to another — Layer, Feature, Hexagonal.

buckpal
|
| - domain
| - Account
| - Activity
| - Accountrepository
| - AccountService
|
| - persistence
| - AccountRepositoryImpl
|
| - web
| - AccountController

3 reasons why this package structure is suboptimal —

  1. There is no package boundary between functional slices of our application. If we add a feature for managing users, we will add UserController to the web package, a UserService, UserRepository and User to the domain package and a UserRepositoryImpl to the persistence package. Without further structure, this might quickly become a mess of classes, leading to unwanted side effects between the supposedly unrelated features of the application.
  2. We can’t see what use cases our application provides. If we are looking for a certain feature, we have to scan through the code and guess what service implements it.
  3. We can’t glace at what functionality is called by the web adapter and what functionality the persistence adapter provides for the domain layer. The incoming and outgoing ports are hidden in the code.
buckpal
|
| - account
| - Account
| - AccountController
| - AccountRepository
| - AccountRepositoryImpl
| - SendMoneyService

Each group of feature in this structure will get a new high-level package next to account and we can enforce package boundaries between features by using package-private visibility to avoid unwanted dependencies between the features.

This structure solves our #1 & #2 concern with Organizing by Layers.

In addition to #3, there is another concern with this package structure. Even though we have inverted dependencies between the domain code and the persistence code so that SendMoneyService only knows the AccountRepository interface and not its implementation, we cannot use package-private visibility to protect the domain code from accidental dependencies to persistence code.

It would be nice if we could point a finger at a box in an architecture diagram and instantly know what part of the code is responsible for that box.

In a hexagonal architecture, we have entities, use cases, incoming and outgoing ports and incoming and outgoing adapters as our main elements.

buckpal
|
| - account
| - adapter
| | - in
| | | - web
| | | - AccountController
| | - out
| | | - persistence
| | | | - AccountPersistenceAdapter
| | | | - SpringDataAccountrepository
| |
| - domain
| | - Account
| | - Activity
| |
| - application
| - SendMoneyService
| - port
| - in
| | - SendMoneyUseCase
| - out
| - LoadAccountPort
| - UpdateAccountStatePort

At the highest level, we have a package named account, indicating that this is the module implementing the use cases around an Account.

On the next level, we have —

  1. domain package containing our domain models
  2. application package. SendMoneyService implements the incoming port interface, SendMoneyUseCase, and uses the outgoing port interfaces, LoadAccountPort and UpdateAccountStatePort, which are implemented by the persistence adapter.
  3. adapter package. Incoming adapters call the application layers incoming ports. Outgoing adapters provide implementations for application layers outgoing ports.

Imagine if we are talking to a colleague about modifying a client to a third-party API we are consuming, we can point at the corresponding outgoing adapter on the Box diagram to better understand each other and then sit in front of our IDE and start working on the client right away.

In this code structure, the package exactly matches the Box diagram of the architecture.

But doesn’t having so many packages mean that everything has to be public?

For adapter package, all the classes can be package-private since they are not called by the outside world except over the port interfaces that live within the application package.

Within the application package, the ports must be public because they are accessible to the adapters. The services need not be public, because they are hidden behind the incoming port interfaces.

Moving adapter code to it’s own package has the added benefit that we can very easily replace one adapter with another implementation, should the need arise. Say we want to change our database from a SQL db to a key-value db. We simply implement all the relevant outgoing ports in a new adapter package and then remove the old package.

But who provides the application with the actual objects that implement the port interfaces? We don’t want to instantiate the ports manually within the application layer because we don’t want to introduce a dependency to an adapter.

This is where dependency injection comes into play. We introduce a neutral component that has a dependency on all layers. This component is responsible for instantiating most of the classes that make up our architecture.

The neutral dependency injection component would create instances of the AccountController, SendMoneyService and AccountPersistenceAdapter classes. Since AccountController requires a SendMoneyUseCase interface, the dependency injection will give it an instance of the SendMoneyService class during construction. The controller doesn’t know that it actually has a SendMoneyService instance since it only needs to know the interface.

Software Developer by day, Equity Investor by night