mscharhag, Programming and Stuff;

A blog about programming and software development topics, mostly focused on Java technologies including Java EE, Spring and Grails.

Monday, 20 September, 2021

From layers to onions and hexagons

In this post we will explore the transition from a classic layered software architecture to a hexagonal architecture. The hexagonal architecture (also called ports and adapters architecture) is a design pattern to create loosely coupled application components.

This post was inspired by a German article from Silas Graffy called Von Schichten zu Ringen - Hexagonale Architekturen erklärt.

Classic layers

Layering is one of the most widely known techniques to break apart complicated software systems. It has been promoted in many popular books, like Patterns of Enterprise Application Architecture by Martin Fowler.

Layers allows us to build software on top of a lower level layer without knowing the details about any of the lower level layers. In an ideal world we can even replace lower level layers with different implementations. While the number of layers can vary we mostly see three or four layers in practice.

Here, we have an example diagram of a three layer architecture:

The presentation layer contains components related to user (or API) interfaces. In the domain layer we find the logic related to the problem the application solves. The database access layer is responsible database interaction.

The dependency direction is from top to bottom. The code in the presentation layer depends on code in the domain layer which itself does depend on code located in the database layer.

As an example we will examine a simple use-case: Creation of a new user. Let's add related classes to the layer diagram:

In the database layer we have a UserDao class with a saveUser(..) method that accepts a UserEntity class. UserEntity might contain methods required by UserDao for interacting with the database. With ORM-Frameworks (like JPA) UserEntity might contain information related to object-relational mapping.

The domain layer provides a UserService and a User class. Both might contain domain logic. UserService interacts with UserDao to save a User in the database. UserDao does not know about the User object, so UserService needs to convert User to UserEntity before calling UserDao.saveUser(..).

In the Presentation layer we have a UserController class which interacts with the domain layer using UserService and User classes. The presentation also does have its own class to represent a user: UserDto might contain utility methods to format field values for presentation in a user interface.

What is the problem with this?

We have some potential problems to discuss here.

First we can easily get the impression that the database is the most important part of the system as all other layers depend on it. However, in modern software development we no longer start with creating huge ER-diagrams for the database layer. Instead, we usually (should) focus on the business domain.

As the domain layer depends on the database layer the domain layer needs to convert its own objects (User) to objects the database layer knows how to use (UserEntity). So we have code that deals with database layer specific classes located in the domain layer. Ideally we want to have the domain layer to focus on domain logic and nothing else.

The domain layer is directly using implementation classes from the database layer. This makes it hard to replace the database layer with different implementations. Even if we do not want to plan for replacing the database with a different storage technology this is important. Think of replacing the database layer with mocks for unit testing or using in-memory databases for local development.

Abstraction with interfaces

The latest mentioned problem can be solved by introducing interfaces. The obvious and quite common solution is to add an interface in the database layer. Higher level layers use the interface and do not depend on implementation classes.

Here we split the UserDao class into an interface (UserDao) and an implementation class (UserDaoImpl). UserService only uses the UserDao interface. This abstraction gives us more flexibility as we can now change UserDao implementations in the database layer.

However, from the layer perspective nothing changed. We still have code related to the database layer in our domain layer.

Now, we can do a little bit of magic by moving the interface into the domain layer:

Note we did not just move the UserDao interface. As UserDao is now part of the domain layer, it uses domain classes (User) instead of database related classes (UserEntity).

This little change is reversing the dependency direction between domain and database layers. The domain layer does no longer depend on the database layer. Instead, the database layer depends on the domain layer as it requires access to the UserDao interface and the User class. The database layer is now responsible for the conversion between User and UserEntity.

In and out

While the dependency direction has been changed the call direction stays the same:

The domain layer is the center of the application. We can say that the presentation layer calls in the domain layer while the domain layer calls out to the database layer.

As a next step, we can split layers into more specific components. For example:

This is what hexagonal architecture (also called ports and adapters) is about.

We no longer have layers here. Instead, we have the application domain in the center and so-called adapters. Adapters provide additional functionality like user interfaces or database access. Some adapters call in the domain center (here: UI and REST API) while others are outgoing adapters called by the domain center via interfaces (here database, message queue and E-Mail)

This allows us the separate pieces of functionality into different modules/packages while the domain logic does not have any outside dependencies.

The onion architecture

From the previous step it is easy to move to the onion architecture (sometimes also called clean architecture).

The domain center is split into the domain model and domain services (sometimes called use cases). Application services contains incoming and outgoing adapters. On the out-most layer we locate infrastructure elements like databases or message queues.

What to remember?

We looked at the transition from a classic layered architecture to more modern architecture approaches. While the details of hexagonal architecture and onion architecture might vary, both share important parts:

  • The application domain is the core part of the application without any external dependencies. This allows easy testing and modification of domain logic.
  • Adapters located around the domain logic talk with external systems. These adapters can easily be replaced by different implementations without any changes to the domain logic.
  • The dependency direction always goes from the outside (adapters, external dependencies) to the inside (domain logic).
  • The call direction can be in and out of the domain center. At least for calling out of the domain center, we need interfaces to assure the correct dependency direction.

Further reading

Leave a reply