Hexagonal (Ports & Adapters) Architecture

Tugce Konuklar Dogantekin
idealo Tech Blog
Published in
5 min readOct 12, 2020

--

In this article, I want to talk about the hexagonal architecture with some code examples in Java.

The hexagonal architecture was defined by Alistair Cockburn in 2005. Cockburn later named it “Port and Adapter Pattern”, but most people still prefer to use the former name as Hexagonal architecture. Here you can find the original documentation about this topic.

What Is the Hexagonal Architecture?

The hexagonal architecture divides the system into loosely-coupled interchangeable components; such as application core, user interface, data repositories, test scripts and other system interfaces etc.

In this approach, the application core that contains the business domain information and it is surrounded by a layer that contains adapters handling bi-directional communication with other components in a generic way.

Hexagonal Architecture

The hexagon is not a hexagon because the number six is important, but rather to allow the people doing the drawing to have room to insert ports and adapters as they need, not being constrained by a one-dimensional layered drawing. The term hexagonal architecture comes from this visual effect.

We can say Hexagonal architecture is a model of designing software applications around domain logic to isolate it from external factors.

Why Use Hexagonal Architecture ?

This approach helps us to achieve to make the business (domain) layer independent from framework, UI , database or any other external components.

This makes business logic testable the business logic without any dependence to the other systems. This means the business rules can be tested without the UI, Database, Web Server, or any other external element.

This also gives flexibility to make changes on the adapters easily. For example you can swap out Oracle or SQL Server, for Mongo or something else. Your business rules are not bound to the database.

This pattern allows to isolate the core logic of application from outside concerns. Having the core logic isolated means you can easily change data source details without a significant impact or major code rewrites to the codebase. Your business rules simply don’t know anything at all about the outside world.

To sum up, this approach will make your application more testable, manageable and easily maintainable.

Now let’s check the important elements of this architectural style. While doing this, we will also work on a simple bank account application to have a better understanding.

Domain Object :

The domain object represents core domain model and is the core of the application. It can have state and business behaviour. The domain object does not have any dependency on the other components, that's why any changes in the other components do not affect the domain object. When a business requirement changes, then the domain object is modified to reflect the change.

Let's create a domain object for the account domain object:

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private Long id;
private String accountId;
private String name;
private String owner;
private BigInteger balance;
private Instant createdAt;

public void withdraw(final BigInteger money) {
this.setBalance(this.getBalance().subtract(money));
}

public void deposit(final BigInteger money) {
this.setBalance(this.getBalance().add(money));
}
}

In the Account object, you can see the fields and some behaviour (deposit & withdraw) of the Account object.

If you want to make a simple domain object class, you can design domain object without any behavioural methods and create use cases for each behaviour of the domain object, it is up to you.

Use Cases:

Use cases are the abstract definition of what the user would like to do in your application. Just like the domain objects, use cases are also a part of the application core and do not have any dependency on the other components. All the business logic, validations are happening in the use of case classes.

In our account service example, I prefer create two different use case interfaces, one of them is AccountUseCase and TransferMoneyUseCase to show you an example the usage of loosely-coupled use case components.

  • Note: I used the name UseCase to show the structure, but my personal preference is to use the name ofService
public interface AccountUseCase {
Account retrieveAccountById(final Long id);
List<Account> retrieveAccounts();
Account create(final @Valid CreateAccountCommand command);
void delete(final Long id);
Transfer transferMoney(@Valid final SendMoneyCommand command);
}
public interface TransferMoneyUseCase {
Transfer transferMoney(@Valid final SendMoneyCommand command);
}

When you check the implementation classes of use cases you will see how we use output ports to reach database layer.

Input And Output Ports:

Application core uses dedicated interfaces called “ports” to communicate with the outside world. They allow the entry or exiting of data to and from the application.

An input port (driving port) lets the application core to expose the functionality to the outside of the world.

An output port (driven port) is another type of interface that is used by the application core to reach things outside of itself (like getting some data from a database).

In our code, AccountUseCase interface is an input port. AccountRepository and TransferRepository interfaces are our output ports.

Account Repository :

public interface AccountRepository {
List<Account> findAll();
Optional<Account> findById(final Long id);
Optional<Account> findByAccountId(final String accountId);
Account update(final UpdateAccountCommand command);
Account save(final InsertAccountCommand command);
void delete(final Long id);
}

TransferRepository:

public interface TransferRepository {
Transfer save(final InsertTransferCommand command);
}

Adapters:

As we mentioned in the beginning, there is an adapter layer that surrounds the application core. Adapters in this layer are not part of the core but interact with it.

There are two types of adapters. The primary adapters use the input ports to trigger the execution of use cases. For example in a mobile application when a user clicks a button, relevant adapter should call the input adapter to request for the associated use case. The secondary adapters are called by the use cases. For example a secondary adapter can be used by the use case to access a certain data from a database.

Primary / Driving Adapters:

In our example AccountController and MoneyTransferController class represents the Web UI as primary adapter, which is exposing our application via a REST API. It is our input adapter. When ever we want to change from Web to Console UI, we can make the changes without interact with the domain model.

Secondary / Driven Adapters:

In our example, SQL is the secondary adapter.

Sample Account Service Structure

In our example, we divided 3 parts in our application; Web Framework Layer, Domain layer ( Core) and Infrastructure Layer. There are other ways to do it, you can try to create your own way.

Here is the GitHub Repository you can find the whole solution.

See you in the next article.

Do you love agile product development? Have a look at our vacancies.

--

--