During our training in architecture, we often ask participants if they are familiar with Hexagonal architecture, and it’s surprising to see only a few hands raised initially. After a brief presentation, we see a few more hands go up, as participants realize they have been using Hexagonal architecture without realizing it.

As hands-on architects, we rely heavily on Hexagonal architecture in our daily work. However, we notice that many people are still unaware of the significant benefits it can bring to their projects. 

Let’s find out together the reasons why you should consider adopting Hexagonal architecture today.

Why is it important to consider it?

Before diving into Hexagonal architecture, let’s understand the following concepts:

  1. Application code: This refers to business logic focusing on the core functionality of the software. That can be tax calculation rules, a process of user registration, and so on.
  2. Infrastructure code: This deals with specific technologies that support the application code. Essentially, any SQL query, an HTTP request, an SMTP call, or anything related to a specific technology fits into this category.
  3. Separation of concerns: An established principle in programming that states that distinct aspects of a software system should be separated and modularized to promote maintainability, readability, and reusability.

Separating application code and infrastructure code is important for several reasons: 

  • First, it simplifies testing. Let’s consider an application used for tax calculation that needs to retrieve some data to provide the proper tax rate from the database. When application and infrastructure code are separated, creating straightforward automated tests for tax calculation rules becomes much easier. However, if those were tied together, the database must be running along with the tests, making them significantly slower.

 

  • Second, it enhances flexibility. Consider a real-life example where a team needed to replace one messaging system with another (Apache Artemis -> Apache ActiveMQ). By keeping the messaging-specific code separate from the application code, the transition could take much less time, and the core functionality remained unchanged. Had the messaging system been tightly integrated with the application code, this would have been a much more challenging task.

 

  • Third, it promotes reusability. Consider what happens when a development team is asked to support a new type of client for the application - for instance, in addition to HTTP over REST, we now want to support SOAP. If the application code was mixed with the infrastructure code (specific to HTTP over REST), it would be difficult to accommodate this change. However, if we kept them separate, we could add new code supporting SOAP while reusing all the existing application code.

 

  • Additionally, the separation of concerns allows for better maintainability, as it becomes easier to identify and fix issues without affecting other parts of the system.

In conclusion, keeping application and infrastructure code separate is crucial for effective software development. 

Hexagonal architecture is a solution that promotes this separation, ensuring maintainability, flexibility, testing, and high cohesion, so read on :).

What is Hexagonal Architecture?

Hexagonal architecture, also known as Ports and Adapters, is an architectural pattern that enables the separation of business logic from infrastructure-related aspects such as HTTP, databases, file systems, and others.

You will soon discover that Hexagonal architecture is not a revolutionary new concept, but rather a well-established principle closely aligned with other patterns such as Clean Architecture, Dependency Inversion, and general Object Oriented Programming (OOP) principles.

The following picture (among many available on the internet) illustrates the main idea:

The picture above illustrates a distinct separation between Ports and Adapters, with the Application Code being only aware of the Ports.

A Port is a technology-agnostic interface in your code. The Driving Port allows inputs (for example, an HTTP call) to come to the Application Code, while the Driven Port enables the Application Code to work with the outputs (such as saving data to a database or sending a message to Kafka).

On the other hand, an Adapter is a technology-specific implementation that allows interaction with real devices like databases, message brokers, browsers, SMTP servers, and so on. Adapters depend on Ports and have no direct relationship with the Application Code in your system. 

By the way, REST Controllers are an example of an HTTP Adapter, therefore they should never contain any business logic.

In the next section, we will examine the same picture accompanied by a code example.

Example

In the example, you will see a conceptual picture and a folder structure that you might have in your project, as well as the implementation. However, please note that you may need to adapt it to your specific needs. For example, you may need to add common classes, and additional attributes, or make other modifications since this example is simplified for clarity and is agnostic of the technical stack.

The following picture is similar to the one you have already seen, but it shows specific implementations for Ports and Adapters:

BookController is an HTTP Adapter.

CreateBookHandler is a Driving Port.

BookRepository is a Driven Port.

JPABookRepository is a Persistence Adapter (JPA stands for Java Persistence API).

Now, let’s take a look at a possible folder structure:

The picture above focuses on the module structure and dependencies between components. As you can see, the classes within the Application and Domain folders are unaware of adapters and are only responsible for business use cases.

One important note: adapters of different types should not depend on each other! Otherwise, if you decide to replace, for instance, the persistence adapter, you would also need to modify the HTTP adapter.

Finally, let’s take a look at a possible implementation.

The BookController adapter accepts an HTTP request and finds a port (CreateBookHandler) that can handle the request:

CODE
package com.trainitek.sample.book.infrastructure.web;

@RestController
public class BookController {

    private final CreateBookHandler createBookHandler;

    @PostMapping
    public ResponseEntity<String> create(CreateBookRequest request) {

        var command = new CreateBook(request.title(), request.author());

        var id = createBookHandler.handle(command);

        return ResponseEntity.created(
                URI.create("/api/books/%s".formatted(id)))
                .build();
    }
}

The CreateBookHandler handles the business use case by creating a new book and saving it using the BookRepository port:

CODE
package com.trainitek.sample.book.application;

public class CreateBookHandler {

    private final BookRepository repository;

    public UUID handle(CreateBook command) {
        var book = new Book(command.title(), command.author());
        return repository.save(book).getId();
    }
}

BookRepository is just an interface:

CODE
package com.trainitek.sample.book.domain;

public interface BookRepository {

    Book save(Book book);

    Optional<Book> find(UUID id);
}

Its database-specific implementation (JpaBookRepository) resides under the infrastructure/persistence folder, completely separate from the application code:

CODE
package com.trainitek.sample.book.infrastructure.persistence;

public class JpaBookRepository implements BookRepository {

    private final EntityManager entityManager;

    @Override
    public Book save(Book book) {
        entityManager.persist(book);
        return book;
    }

    ...
}

The presented example scales very well in most projects, and helps achieve high-quality testable and replaceable code!

Hexagonal Architecture in the Microservices World

Even though Hexagonal Architecture has “architecture” in its name, we would argue that it isn’t architecture per se, but rather a useful tactical pattern. It focuses only on structuring and organizing code and components inside a specific service.

Take a look at the picture below. We have two hexagons that communicate through a messaging system. Hexagonal Architecture doesn’t address the high-level picture: it doesn’t suggest how to communicate, nor does it define the structure of systems consisting of multiple services. So, we like to think about hexagons as being applicable inside specific services, not as a high-level architecture.

Note: In this article, we will stick to the name Hexagonal Architecture (or Hexagon) as it’s already “embedded” in the software development vocabulary.

Should we use it everywhere? 

As with most software development patterns or methodologies, it’s impossible to say that one can be used universally, as there are always caveats and exceptions. So, when is Hexagonal architecture a good fit?

As a rule of thumb, consider Hexagonal architecture as a good choice for:

  1. Long-lasting applications that need to be maintained for an extended period. Maintainability costs tend to rise over time in non-hexagonal systems.
  2. Projects that require the flexibility to test new technologies (such as databases, messaging systems, or cloud providers).
  3. Larger and more complex projects, which are more likely to benefit from Hexagonal architecture.
  4. Projects where testing is essential, and you want to test the application and domain logic separately from the infrastructure code.

However, a Hexagonal architecture may not be advised in certain situations:

  1. A fairly simple project (sometimes referred to as CRUD). In these cases, a slightly less elegant solution may still provide good value for the efforts spent on implementation.
  2. A quick prototype is needed to secure a client for building a more extensive system (and when it’s won, then consider using Hexagonal architecture).
  3. A framework-specific library or system is being developed, where you want to tie yourself to the infrastructure-specific code to utilize it fully.

As you can probably guess, many projects written in mainstream languages fit into the first list, suggesting that you should consider using Hexagonal architecture.

Illusions of Hexagonal Architecture 

Illusion number 1: “I can easily switch between sync/async ways of communication with my infrastructure”.

This must be the biggest illusion (or trap) software engineers fall into when applying Hexagonal Architecture in their projects. With good intentions, developers (ourselves included) attempt to create perfectly reusable interfaces that hide away the details of whether an operation is asynchronous or synchronous. This is a mistake.

Consider this example:

The question is whether we can create the BookRepository interface and hide the asynchronicity of the operation behind the Adapter.
In the case of an AsyncBookRepository adapter, we might not be sure if, just after saving the book, we are guaranteed to “find” it in the next operations. The SyncBookRepository, however, will return the book as we expected. In both options, the observable results can differ.

A much better approach is to expose the asynchronicity of the operation at the Port level. That is why in many frameworks or libraries, you can find both synchronous and asynchronous methods for the same operations.


Conclusion: Prefer exposing async support on the port level.

Illusion number 2: *“I can easily switch to infrastructure with different ACID properties”.
*
Consider the scenario presented in the image below:\


Depending on our Adapter implementation, we might get different results regarding transaction boundaries.\

  • In the case of MySQL (or any other datastore offering ACID properties), both books will be saved if the transaction succeeds, or neither will be saved in the case of a rollback.\
  • If we choose to switch to another Adapter - e.g., based on ElasticSearch (or similar tools like OpenSearch or Azure AI Search) - we might get different behavior, such as only one book being saved if a failure occurs between the two operations.

    We need to be aware of the guarantees of our datastores and design transaction boundaries accordingly.

    Conclusion: The main idea is to separate application logic from infrastructure code, rather than to seek a silver bullet for universal architecture.

Illusion number 3: *“I can’t have library-specific annotations/attributes on the domain classes”.
*
Consider the following piece of Java code that uses the Java Persistence API:


When applying Hexagonal Architecture, many developers attempt to follow it to the letter. If we do that, we might ban our persistence framework annotations from our Application code, arguing that they should be hidden behind an Adapter implementation. Following this path has a high price to pay; there is a lot of additional complexity and work needed to achieve that, such as adding additional abstraction layers, duplicating many classes, providing additional mappings and translators, ensuring proper test coverage, and providing utility classes.

We suggest being pragmatic and asking yourself these questions when making this decision:

  • How often do you change a chosen persistence framework?
  • Do annotations/attributes help you become faster or less effective?
  • Can you test your code without running any infrastructure?
  • Do you clearly see the value of being a purist?

Conclusion: Hexagonal Architecture is a tool, and it’s your responsibility to apply it in the most effective way to meet your needs.

An ‘Important’ Question: “Why Does It Have 6 Sides?”

There is no direct answer to this question. Here’s what we think:

  • Circles, boxes, and rectangles are commonly represented in various contexts.
  • Drawing 5 sides can be more challenging.
  • Having more than 6 sides would be considered “too much”.
  • Finally, it doesn’t matter much; it’s only a name.

How Is It Different from Clean and Onion Architectures?

Hexagonal, Clean, and Onion architectures share the core principle of organizing code for maintainability, flexibility, and testability, representing fundamental design concepts, such as the Dependency Inversion Principle (code should depend on abstractions rather than on implementations). Hexagonal architecture focuses on separating business logic from infrastructure code through ports and adapters, making it less generic and more straightforward to explain. 

In contrast, Clean and Onion architectures emphasize the separation of concerns through concentric circles or layers, with core business logic at the center. All three architectures promote the same fundamental OOP concepts, including Dependency Inversion, and should become the default way of thinking about code organization. Despite their differences, each pattern is a valuable tool for creating clean, maintainable, and flexible code.

Conclusion

If, after reading this article, you think that the principle is very simple and you understand it, you might be right. However, don’t be fooled into thinking it will be easy to follow in real projects. Even though team members remember the rule, time after time, we find our infrastructure code “leaking” into application code, making testing and maintenance more challenging. Additionally, the entire team must be taught Hexagonal architecture rules and apply them consistently in their daily work. Last but not least, don’t fall into the illusions of Hexagonal Architecture.

Overall, we should view Hexagonal Architecture as a powerful tool in our toolbox, applying it in cases where we need to ensure flexibility, testability, reusability, and maintainability.

This article is part of the JAVAPRO magazine issue:

From Coder To System Designer

Understand what it means to move from coding to designing systems in the age of AI.
Take a closer look at modern Java platforms, architectural thinking, and the responsibilities that come with shaping complex software systems.

Discover the edition