22/01/2023
10 min read
Architecture

Content:

Hexagonal Architecture a productivity tool





Introduction


Hexagonal architecture, also known as ports and adapters, or clean architecture, is an architectural design pattern that aims to separate essential complexity from accidental complexity. In other words separate the buisiness logic ( the heart of your application) and its external dependencies like the database, webservices, local storage, frameworks etc… this architecture is particulary useful for creating robust applications that are easy to test, maintain, and extend.

Alistair Cokckburn invented this archirtecture you can find his article about it here.


Case study the banking kata




Requirements

Write a class Account that offers the following methods :

void deposit(int) void withdraw(int) void printStatement()

An example statement would be:


Date Amount Balance

24/08/2015 +500 500

24/08/2015 -100 400


I solved this kata in Java using TDD and hexagonal architecture and i think this kata is a good start to understand hexagonal architecture.



Analyse

This kata is more hard that it seems at first glance , we can already spot that the deposit, withdraw, and printStatement method are commands because they don't

return anything so they will be hard to test.

We can already identify two external dependencies: the console that will allow us to print the transactions on the screen and the date because it is non deterministic. Even though that Alistair in his original article doesn't tell us how many layers there are in the hexagon i will walk us through how i solved this kata using hexgonal architecture by showing you and explaining you the goal of each layer.



Hexagon


The Hexagon is the core of our application, it is where the essential complexity live, this the brain of our product which encapsulate our buisiness logic.

No Infrastruscture code in it , no database, no webservices … But in it you can have an abstraction usually an interface or type that will represents a more abstract form of the database , we usally call it repository for exemple.

It worth noting that in the hexagon we can write our code with the programming paradigm that we want: functional programming, object oriented programming and even imperative programming.

In the Hexagon we will often find these three layers.



Domain

In it you will find the buisness objects that encapsulate buisness data and buisness rules.

They don't depends on frameworks or other dependency ( i'm pragmatic with this rule and often pass to my enties a dateProvider as we will see in the code exemples).

For this kata the entity that i made emerge is a transaction (deposit and withdrawal) which represent the data that are printed on the screen by the printStatment method.


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import bankingKata.core.dateProvider.IDateProvider; import java.text.SimpleDateFormat; public class Transaction { private final IDateProvider dateProvider; private final double amount; private final boolean isDeposit; private final double balance; public Transaction(IDateProvider dateProvider,double amount,double balance,boolean isDeposit) { this.dateProvider = dateProvider; this.amount = amount; this.balance = balance; this.isDeposit = isDeposit; } public String getDate() { return getFormattedDate(); } public String getAmount() { return isDeposit ? "+" + amount : "-" + amount ; } public double getBalance() { return balance; } private String getFormattedDate() { SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy"); return formatter.format(dateProvider.now()); } }



Uses-cases/application/interactors

It's the layer where we implement the orchestration of our requirements ,in our case this is where we implement the feature of deposit , withdrawal and the transactions printing features.

Still no frameworks , or database and other technologies specific stuff here.

This is by this layer that we usually start coding and also this layer that we target in our tests because this layer will use the port and the entities layers.

That unable us to make very robust tests that are coupled to behavior and not implementation.

The use cases part often got its dependencies injected so it does not know which dependencies it uses, that is call dependency inversion it is a powerful concept, in fact this is the fith SOLID principle.

this concept unable us to swap our dependencies easily, we can easily change of database for exemple.

Thank's to this principle testing is really easy because we can swap the real dependency with a fake one in our tests.

Let's look at the implementation of this part for the banking kata.



1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package bankingKata.core.useCases; import bankingKata.core.dateProvider.IDateProvider; import bankingKata.core.entities.Transaction; import bankingKata.core.logger.ILogger; import java.util.ArrayList; import java.util.List; public class Account { private final ILogger logger; private final IDateProvider dateProvider; private final List<Transaction> transactions = new ArrayList<>(); private double currentBalance; public Account(ILogger logger, IDateProvider dateProvider) { this.logger = logger; this.dateProvider = dateProvider; } public void printStatement() { String transactionsToPrint = transactions.stream() .map(this::buildTransactionsToPrint) .reduce("Date Amount Balance", String::concat); logger.log(transactionsToPrint); } private String buildTransactionsToPrint(Transaction transaction) { return "\n" + transaction.getDate() + " " + transaction.getAmount() + " " + transaction.getBalance(); } public void deposit(double money) { currentBalance += money; performTransaction(money,true); } public void withdraw(double money) { currentBalance -= money; performTransaction(money,false); } private void performTransaction(double money, boolean isDeposit){ Transaction transaction = new Transaction(dateProvider,money,currentBalance,isDeposit); transactions.add(0,transaction); } }


Port

These are Types or Interfaces that live at the edge of the hexagon. Their goal is to enable the hexagon to communicate with the outside world.

This layer is an abstraction that shield us by forcing the dependency to fufill a contract. In our case our denpendencies are the console and the current date.


1 2 3 4 5 6 package bankingKata.core.logger; public interface ILogger { void log(String dateAmountBalance); }


1 2 3 4 5 6 7 8 9 package bankingKata.core.dateProvider; import java.util.Date; public interface IDateProvider { Date now(); }


Adapters/Infrastructure


Adapters are the functions are classes that deals with the outside wolrd, this is where you're gonna make your database queries and your api calls etc…

in our case with got a dateProvider, and a logger adapters.

Actually we've got four adapters, we have also their in memory counter parts a fake date provider and a fake logger that we use in our tests.

You'll notice that each adapter implements the port that we saw earlier this is how we know that they respect their contracts.

For more complex dependencies like database i also make focus integration tests that target the real dependencies ,

that means that i will use a real database to be sure that my production code will work when i will swap the fake dependency with the real one.

Let's see our four adapters for the banking kata.


1 2 3 4 5 6 7 8 9 10 11 package bankingKata.adapter.dateProvider; import bankingKata.core.dateProvider.IDateProvider; import java.util.Date; public class DateProvider implements IDateProvider { public Date now(){ return new Date(); } }


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package bankingKata.adapter.dateProvider; import bankingKata.core.dateProvider.IDateProvider; import java.util.Date; public class InMemoryIDateProvider implements IDateProvider { private Date date; public InMemoryIDateProvider(Date date) { this.date = date; } public Date now(){ return date; } }


1 2 3 4 5 6 7 8 9 10 11 package bankingKata.adapter.logger; import bankingKata.core.logger.ILogger; public class Logger implements ILogger { public void log(String dateAmountBalance){ System.out.println(dateAmountBalance); } }


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package bankingKata.adapter.logger; import bankingKata.core.logger.ILogger; import bankingKata.tests.Spy; public class InMemoryLogger implements ILogger { private final Spy spy; public InMemoryLogger(Spy spy) { this.spy = spy; } @Override public void log(String dateAmountBalance) { spy.recordValue(dateAmountBalance); } }


Primary/Driving/Inbound Adapters

There is two type of adapters , the primary adapter is the one that will call the use-case ,that is why it is also called driving or outbound adapter.

This kind of adapter in the backend world if you use Java it is likely to be Spring or if you use Javascript/Typescript Nest js or Express js.

In the Frontend world it will be React, Vue or Angular etc…


Secondary/Driven/Outbound Adapters

This type of adapter is the one that is called by the use-case, it will often be the database adapter in the backend world for exemple. you can see that this is the only kind of adapter that we have in our banking application.


Conclusion




You can find the repo that contains my solution for the banking kata here.