Hexagonal architecture made simple – practice

The context

In my previous post, I presented a bit of theory on hexagonal architecture. It was not a typical theoretical description of the issue, as there are plenty of such available on the Internet.
I am going to present an implementation, which I myself would call hexagonal-ish.
First, let’s define basic requirements.

Basic requirements

The project we are to create is the MVP version. Our stakeholders strongly believe in its success, but would like to validate their perceptions with the market as soon as possible. We don\’t expect any big traffic at first, a few rentals a day will be absolutely satisfactory. However, if the project proves to be a success, within the next year our founders will invest a huge amount of money in marketing to get clients all over the world.

The constraints

Based on our requirements we’re able to extract several most important constraints for our application design:

  • We don’t have time to experiment with the latest technologies. We need to deliver the MVP as soon as possible.
  • Our solution, although it is an MVP version, must meet the basic functionality- it will be launched in production and customers will use it.
  • Our app has great potential for growth, so we must be ready to scale our system.

The project definition

For a video rental store we want to create a system for managing the rental
administration. ​ We want three primary features:

  • Have an inventory of films
  • Calculate the price for rentals
  • Keep track of the customers “bonus” points

Price

The price of rentals is based on the type of film rented and how many days the film is
rented for. The customers say when renting for how many days they want to rent for
and pay up front. If the film is returned late, then rent for the extra days is charged
when returning.
The store has three types of films.

  • New releases – Price is times number of days rented.
  • Regular films – Price is for the first 3 days and then times the number of days over 3.
  • Old film – Price is for the first 5 days and then times the number of days over 5

premium price is 40$
basic price is 30$

Examples of price calculations

Matrix 11 (New release) 1 days 40$


Spider Man (Regular rental) 5 days 90$


Spider Man 2 (Regular rental) 2 days 30$


Out of Africa (Old film) 7 days 90$


Total price: 250$


When returning films late


Matrix 11 (New release) 2 extra days 80$


Spider Man (Regular rental) 1 days 30$


Total late charge: 110$

Bonus points

Customers get bonus points when renting films. A new release gives 2 points and
other films give one point per rental (regardless of the time rented).

The analysis

At first glance, it seems that we can distinguish the following bounded contexts:

  • Rental
  • Bonus points
  • Inventory

However, this is purely intuition. So let’s try to visualize our assumptions and add the interactions between the mentioned modules.
This approach has an additional advantage- it makes the coupling between components more visible.
We assume that detailed information about the movies (like movie type) are available in the Inventory module. All others use only the identifier received from the outside.

While the above analysis seems to make sense and we have identified our bounded contexts quite well, let\’s try to draw a simple sequence diagram for our most important rental happy path.

The above sequence diagram highlights two aspects.

  • First, each module needs communication with Inventory to find out movie type, since price, surcharge, bonus points are calculated on this basis.
  • The second aspect is where the calculations related to price and surcharge are made. Currently, this takes place in the Rental module.

If we go back to our requirements, we will notice that most of the business logic is related to these operations.
So, we should ask ourselves whether we are sure we have not missed one important module in our analysis….
Let’s think for a moment about the possible directions of development of our application.
Currently, we have three types of movies, each of which is assigned a certain base price.
Based on this, we have algorithms for calculating the price. What we can be sure of in the future are more types of movies and new ways of calculating the price. Perhaps there will be a special price for VIP customers, perhaps there will be special promotions like a fourth movie for free, etc.
Is it really the Rental module that should be responsible for all these operations? In fact, all we need to place a rental order is information about the film identifier. All these hints suggest that we should add another module to our diagram – Pricing.

Let’s go back to the first aspect.
The main goal of our project is to provide the MVP version. However, this does not mean that we have to completely forget about all good and bad practices.
Still, our code should be open for extension but closed for modification.
One technique that will help us in this case is Inversion Of Control also known as the Hollywood Principle (don’t call us we will call you).
So, let’s design our modules in such a way that this Inventory informs everyone else that a movie has been created. Other modules can store their own local copy containing the data they need.

The updated version of the diagram looks much better. It takes into account the inverted dependencies to the Inventory module, as well as the additional bounded context- Pricing.

It’s time to hammer the keyboard

Are we ready to sit down and implement our solution? Almost…
What remains to be decided on is the application architecture itself.
Taking into account all our requirements and constraints, we will start building a monolith. However, we are concerned with the good organization of the code, so that future migration to microservices will be smooth. Therefore, we will build modular monolith with modularization on package level.
This means that we do not need to use the same architecture patterns for each of them.

One last important detail

In order for our modules to be truly independent, we must also ensure that they do not share database tables with each other.
Yes, this implies some duplication of data. However, this is a price worth paying for the low coupling between modules and their high cohesion.

Key implementation details

Packages will serve as modules of our application. We could create a separate .jar file for each of them, but at this stage I don\’t see much benefit from such a solution.

Each package has a structure:

  • api, here lands all the classes that are used to communicate with external systems/enable communication with a given module
  • domain, the heart of our package. This is where we mostly make sure that our objects are “clean” and free of dependencies to frameworks and the outside world. However, this does not always have to be the case. Sometimes our package implements simple CRUD operations, so we can make it full of dependencies.
  • infrastructure, implementing all the ports and adapters from the domain package, i.e. repositories to the database, queues, etc.
  • web, http api. Sometimes missing if there is no need for an endpoint.

Which module represents the port and adapters approach?

In our sample application, most of the business logic is related to price calculation. Therefore, the Pricing module represents the clean architecture approach.

In contrast, the Inventory module is a very simple CRUD, so we will find database entities in the domain package. You may find that refactoring needs to be done in the future, but this is the normal process of evolving architecture.

Demo implementation

A demonstration of the implementation can be found here.

Not everything is perfect there and I definitely wouldn’t call this code production ready. It is for educational purposes only and serves to demonstrate the concept of hexagonal architecture in practice.
I am aware of the multiple implementations, alternative code organization options, package structure presented in books on DDD, etc.. I believe that as long as you and your team are comfortable with certain solutions, and new people joining the team are able to understand them almost immediately, stick with them. There is no silver bullet architecture.
However, of all the different options I’ve worked with, the one presented in the project seems to be very simple on the one hand, whereas on the other it appears to achieve long-term system architecture goals.

Summary

The code implementation is a relatively simple thing. Whether we are dealing with a CRUD-type application or a rich domain model. What seems to be the most difficult is choosing the right solution to the problem. Before we sit down and start typing, it is always worth spending an extra 30 minutes analyzing the problem, finding coupling between components, exploring possible development directions for our system. Such work will save us sometimes even a few days of programming.

Leave a Comment