Hexagonal Architecture for Dummies by a Dummy

gatienducornaud

Gatien Ducornaud

Posted on February 13, 2024

Hexagonal Architecture for Dummies by a Dummy

TL;DR: You’ll discover the advantages of Hexagonal Architecture in:

  • Simplifying Your Code: Transforming intricate coding and testing efforts into manageable, beginner-friendly tasks.
  • Gaining Real-World Insights: Understand how applying Hexagonal Architecture in actual projects not only upholds code quality but also promotes ongoing learning.
  • Plus, benefit from practical code examples I’ve tackled as a junior developer, illustrating these concepts in action!

As a rookie, Hexagonal Architecture can be your best friend 👨‍💻

  • I arrived on my first project with no prior experience in web development and a very academic/theoretical approach to code. It was also the tech lead's first project in such a role. However, he had experience in hexagonal architecture and believed in its power to help produce quality code, even for a rookie. It is the learnings from this experience that I wish to share here.

Hexagonal Architecture in a (non-boring) nutshell 🧠

Generic schematic for hexagonal architecture

As I understand it, hexagonal architecture aims to protect your domain (where your business logic resides) from the real world, thanks to ports. All processes and data need to go through gates (the ports), so even if some aspects outside of the domain change, if they still follow the rules set by the ports, the code in the domain does not need to change.

In practice, the ports are interfaces, and we ensure that everything goes through ports thanks to dependency inversion, where the implementation of the interfaces does not know of other implementations, but rather other ports. Thanks to that, any implementation of a given interface can be swapped out for any other implementation with no consequence to the code.

Hexagonal Architecture to the rescue of junior developers 🛟

Discovering and improving the codebase step by step ⛏️

By design, Hexagonal Architecture enforces SOLID principles, so the scope of each change can be limited to one file. Do you need to add a new route? Just modify a controller! Need to sort your data a different way? Just change the sorting in your domain! Thanks to dependency inversion, you will not have to chase your changes all over your codebase.

For example, we link users with agencies, and those “linkings” can be discarded. For some use-cases, we do not want to return the discarded linkings. To us, it is one line of code:

@Override
public List<Linking> getLinkings(UserId userId) {
    return linkingRepositoryPort
            .getLinkings(userId)
            .stream()
            .filter(linking -> !linking.discarded())
            .sorted((linking1, linking2) -> linking2.createdAt().compareTo(linking1.createdAt()))
            .toList();
}
Enter fullscreen mode Exit fullscreen mode

Implementing new features is facilitated ⚡

The example given previously works fine if you are modifying existing code, but what about a brand new feature? In that case, you can copy the existing structure, as most the boilerplate code is the same. You don’t have to be familiar with Hexagonal Architecture from the start, as adapting existing pattern will (most of the time) result in a new feature that also respects Hexagonal Architecture itself.

For example, when I created the feature I presented in the previous example, I copied the existing pattern we had for Agencies and created (from the database to the route):

  1. The table
  2. The entity to represent the data in the table
  3. The repository interface (with SpringJPA) to get the data from the table
  4. The repository port interface to better use the data form my domain
  5. The repository service implementing the port to actually call the repository to get the data
  6. The controller to create the routes needed for the feature
  7. The feature port to define the actions of the domain
  8. And the service in the domain to implement the feature port

Writing tests is straightforward 🧪

I was always taught that tests were important, but I was never made to write them, so I rarely did, as it was often painful to do so. However, hexagonal architecture simplifies testing tremendously, especially for the more worthwhile domain tests, where you check if your core logic is sound.

Here is an example of one of my tests, intentionally without any context:

@Test
void should_save_linking_if_same_valid_does_not_exists() {
    // GIVEN
    doReturn(Optional.empty())
        .when(linkingRepositoryPort)
        .getValidLinkingId(USER_ID, AGENCY_ID);

    // WHEN
    linkingService.saveLinking(USER_ID, AGENCY_ID);

    // THEN
    verify(linkingRepositoryPort).saveLinking(USER_ID, AGENCY_ID);
}

@Test
void should_not_save_linking_if_there_is_already_a_valid_one() {
    // GIVEN
    doReturn(Optional.of(LINKING_ID))
        .when(linkingRepositoryPort)
        .getValidLinkingId(USER_ID, AGENCY_ID);

    // WHEN
    LinkingId result = linkingService.saveLinking(USER_ID, AGENCY_ID);

    // THEN
    assertThat(result).isEqualTo(LINKING_ID);
    verify(linkingRepositoryPort, times(0)).saveLinking(any(), any());
}
Enter fullscreen mode Exit fullscreen mode

Those tests can be read even without knowledge of the code and also serve to document the intended behavior of the domain. Furthermore, I didn't have to delve into specifics on how to mock my database or set up and tear down a specific object, so tests practically write themselves!

(As a footnote: as you can also see, given Dependency Injection, you already have your Ins and Outs, so it makes it extremely easy to do Test Driven Development, and to check if every main use case of your domain is tested. I personally found it very useful to get into the habit of writing tests, and to write documentation, as the tests are the documentation)

Focusing on small tasks leading to everyday learnings 🔁

I also found Hexagonal Architecture to be a great help in promoting everyday learnings. As you can play with specific functionalities without having to worry about how they'll impact the rest of the code, you can deep dive into specific issues when they arise.

As an example, we wanted to improve the number of queries of repositories made for each request. Given that we could be certain that the resulting data would be used in the same way, I was free to experiment with the way the entities were linked

For example, in my LinkingEntity, whether I have

@ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "agency_id")
@OnDelete(action = OnDeleteAction.CASCADE)
AgencyEntity agency;
Enter fullscreen mode Exit fullscreen mode

or

@ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "agency_id")
@OnDelete(action = OnDeleteAction.CASCADE)
AgencyEntity agency;
Enter fullscreen mode Exit fullscreen mode

does not matter for the rest of the application, and I could see the number of request made in each case, and explore the impact on the repository.

However, there are still difficulties I faced and I would like to highlight so they can be anticipated.

Hexagonal Architecture drawbacks for a Junior Developer 🚧

Hexagonal Lingo can be overwhelming 📚

When I first started on the project, I had all the names mixed up. “What do you mean a Repository is not what implements a Repository Port? Why does an implementation in my domain implement an interface outside of it?”

In order to sort it out, I found it useful to visualize it by placing each type of class or package in the theoretical diagram. This was a good help to link the theoretical aspects to the way we used it in the code. For example, a typical feature would look like this in our case:

Example schematic of use of hexagonal architecture

There are a LOT of patterns 🔢

When I first started to get confused about what went where, I drew a diagram similar to the previous one and thought that I understood how it worked. But then I had to do a request towards another web-service, and I had no idea how to do it. So I copied the API interface I saw for performing GET calls, but there was class implementing such interface. Turned out that the diagram looked like this:

Different example of use of hexagonal architecture

It may not seem very different, but it does imply non-trivial differences in the code. So it implies continuous learning about hexagonal architecture itself, not a just an initial time investment.

Lots of code for simple things 💣

Following learning the new pattern for API, I implemented it for the feature. In order to do that, I created or modified the following:

  • a controller,
  • a new class for the data transfer object,
  • the feature port,
  • its implementation,
  • the object class in the domain,
  • the API,
  • the class for the object how it is received,
  • the configuration to instantiate the API.

In total, I created 6 new files and modified 2 others for what amounted basically to "Call a given service, tweak the return data a bit, and return that," which could have been done in a much shorter way.

Such work for a simple feature can be frustrating, especially it is repeated. There, the theoretical knowledge of the flexibility the hexagonal architecture offers was a good justifier of why such work was required.

Parting words

As you may guess, I would highly recommend using hexagonal architecture on your own projects. But, based on my own experience as a rookie, I would also recommend it if you are in charge of a project with junior developers, so as to help them improve their skills and produce quality code faster.

Happy hexagonal coding! 👨‍💻🌐

💖 💪 🙅 🚩
gatienducornaud
Gatien Ducornaud

Posted on February 13, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related