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 🧠
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();
}
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):
- The
table
- The
entity
to represent the data in the table - The
repository
interface (with SpringJPA) to get the data from the table - The
repository
port interface to better use the data form my domain - The
repository
service implementing the port to actually call the repository to get the data - The
controller
to create the routes needed for the feature - The
feature port
to define the actions of the domain - 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());
}
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;
or
@ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "agency_id")
@OnDelete(action = OnDeleteAction.CASCADE)
AgencyEntity agency;
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:
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:
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! 👨💻🌐
Top comments (3)
As a junior developer navigating the world of web development, you found solace in Hexagonal Architecture, a concept that your tech lead introduced to shield the domain logic from real-world complexities. Reflecting on your experience, you highlighted the advantages of Hexagonal Architecture, including simplified code, enhanced real-world insights, and seamless adaptability for new features. Could you delve deeper into how Hexagonal Architecture promotes test-driven development (TDD) and simplifies the testing process, as evidenced by your practical examples? Additionally, could you share any challenges you faced as a junior developer while implementing Hexagonal Architecture, particularly in terms of terminology and the extensive use of various patterns, and how you overcame these hurdles? FOLLOW BACK FOR INSIGHTFUL DISCUSSIONS
Thanks for the comment, and the clear summary!
Regarding testing, especially for domain features, we usually test by setting the input thanks to mocks of the result of the input interface, and only using method of the interface that is implemented by the class in question (outputs). We internally refer to it to blackbox testing (I don't know if the term is appropriate), as we don't care what happens inside the method, just that the outputs matches the one expected
Therefore, even with stubs of the methods implemented by the class, we can write the tests, so as to test early and fail early.
Regarding difficulties, as for terminology, it was mostly related to naming conventions, as for example, what had classes named "Service" inside and outside the domain, sometimes what was inside the domain was called "Service", sometimes "Usecase"... Since it was different from the terminology I was introduced to (which was very theoretical, I was quite lost with what went where.
For patterns, it was for example that at times there were several way to actually "implement" interfaces (as with the example I gave), or that sometimes we didn't even go into the domain (as for example to expose simple json files), so it didn't follow the neat pattern I was expecting.
For the both of them, the solution was, as you may guess, time. Having someone with experience with hexagonal architecture to look at what I was doing was a great help, but I still made a lot of mistakes. However, I think that given that everything goes through interfaces, even mistakes are contained to one part of the app.
Thanks for the brief explanation :)