Implementation of Hexagonal Architecture or Ports & Adapter Architecture

Background

As developers, at some or the other point, you have worked on legacy software that is not well maintained. You understand the pain to comprehend simple logic written in complex blocks of code. Further, introducing an enhancement or a new feature gives nightmares to developers.

Maintainability is at the heart of good software design. Codebases that are not well maintained become difficult to manage. Not only they become difficult to scale but also give a hard time to a new developer to onboard & hit the ground running.

In the Tech world, everything moves at a rapid pace. Assume that you are owning a legacy project. If the business asks you to ship a new feature A.S.A.P or you want to move from RDBMS to NoSQL, what would be your first reaction?

Thrash your machine?

High test coverage boosts a developer’s confidence to deploy a new release seamlessly. However, if your application & infrastructure logic is intertwined, would it possible to test your business logic in isolation? That will just aggravate your frustration.

Why me?

That’s enough of ranting, let’s have a tour of Hexagonal Architecture along with an illustration. Adopting this pattern will help you improve the maintainability, testability of codebase along with many other benefits.

Introduction to Hexagonal Architecture

In 2006, Alistair Cockburn coined the term Hexagonal Architecture. This architecture is also known as the Ports And Adapters Architecture. In simple words, the idea is to expose multiple endpoints in software for communication. As long as you have the right adapter for your port, your request will always get handled.

It’s analogous to exposing multiple USB ports on a Machine. If you have the right adapter (mobile charger, or Pendrive) that fits into your Machine’s port, you’ll be able to meet your goal.

Adapter

Software is diagrammatically represented in the form of a hexagon & application business logic forms it’s core. It’s surrounded by entities with which it communicates & components that drive it by providing inputs.

In real-world, different entities such as user actions, API calls, automated scripts & unit tests interact with your software and provide various inputs. In case your business logic becomes entangled with user interface code, you’ll observer multiple difficulties. For eg:- It will become cumbersome to switch from user-driven input to a unit test-driven input.

Similarly, an application interacts with external entities such as Databases, message queues, web servers through HTTP API calls, etc. If you want to migrate database or dump data to a file system instead, you should be able to achieve this without touching your business logic.

As the name ‘Ports And Adapter’ suggests, it defines ports which are means through which communication takes place. Adapters are components that handle user input and convert it into language-specific message call in the core. Likewise, adapters encapsulate the logic to interact with external systems such as databases, message queues, etc and facilitate communication between core and external objects.

Working

Let’s go through a deep walkthrough of the pattern. The below diagram shows the different layers in which the application is divided:-

Hexagonal Architecture

Hexagonal architecture divides the application into three layers — Domain, Application & Framework. Following is a brief description of the three layers

Domain — This layer contains the core business logic. It’s not supposed to know the implementation details of the outer layers

— This layer contains the core business logic. It’s not supposed to know the implementation details of the outer layers Application — This layer acts as a glue between the domain and the framework

— This layer acts as a glue between the domain and the framework Framework — It implements how domain interacts with the external world. Inner layers act as a black box for this layer

According to the architecture, two actors — Primary & Secondary interact with the application. The Primary actors send request or drive the application. For example — Users or automated test suite. Secondary actors provide infrastructure to the domain to communicate with external entities. For example — Database adapters, TCP or HTTP client.

Let’s represent the actors in the below diagram:-

Hexagonal Architecture

The left side of the Hexagon consists of drivers (which provide input to the core) while the right side represents all the components that are driven by our application.

Illustration

Let’s design an Application that stores movie reviews. The user can query the application with a movie name. The app returns random five reviews for the given movie.

For simplicity, let’s assume it’s a console app. Movie reviews data is stored in memory. The user response is printed on the console.

We have the User which sends the request to the app. Thus, the User becomes a driver. The app can fetch data from any data store. The application can either write the response on the console or a file. So, data fetcher and response printer becomes the driven entities.

The driver and driven components are explained in the below figure:-

Hexagonal Architecture of Movie Review App

On the left, we have the driver which provides input to the app. Driven components are on the right which enables the App to communicate with Database and console.

Let’s have a quick Code walkthrough of the above App.

Driver Port

Driver Port

Driven Ports

Driven Ports

Driven Port Adapter

The movie fetcher will extract movies from the movies repo. We will have a console printer which will print movie reviews on the console. Let’s implement the above two interfaces.

ConsolePrinter and MovieReviewsRepo

Domain

Our core domain handles user requests. The core will fetch movies, process them and pass the results to the printer. For now, we only have one request i.e search for Movies. We will use the standard Java Consumer interface to handle user requests.

Let’s have a look at our core domain class i.e MovieApp.

Movie App

We will now define a Command mapper which will map the commands with specific handlers.

Command Mapper

Driver Adapter

The user will interact with our system through the IUserInput interface. Let’s implement this interface. The implementation will build a use case model. It will use the model runner and delegate the action.

Primary Actor

We will now take a look at our primary actor the user which will use the above interface for communication.

Movie User

Movie application

We will now create the console application. We will add the driven adapters as a dependency in our application. The user will be creating and sending the request to the application. The application will fetch the data, process and print the response on console.

Movie Application

New features/Changes

In the above example, you could easily switch from one datastore to another with minimal change. Datastore dependency can be injected into the code without altering the existing business logic. For instance, you can move the in-memory data to a database, write & inject a database adapter into the application

Similarly, instead of a Console Printer, you could have a Printer that would write to a file system. It becomes simple to introduce new features and fix bugs in such a layered application

You can write comprehensive tests to test your business logic. Adapters can be tested in isolation. Thus, the overall test coverage of the application can be improved

Conclusion

We have learned the following benefits of adopting hexagonal architecture:-

Maintainability — We build layers that are loosely coupled and independent. It becomes easy to add new features in one layer without affecting other layers.

— We build layers that are loosely coupled and independent. It becomes easy to add new features in one layer without affecting other layers. Testability — Unit tests are cheap to write and fast to run. We can write tests for each layer. We can mock the dependencies while testing. For example:- We can mock a database dependency by adding an in-memory datastore.

— Unit tests are cheap to write and fast to run. We can write tests for each layer. We can mock the dependencies while testing. For example:- We can mock a database dependency by adding an in-memory datastore. Adaptability — Our core domain becomes independent of changes in external entities. For eg:- If we plan to use a different database, we don’t need to change the domain. We can introduce the appropriate database adapter.

References