Hexagonal architecture with Golang (part 1)

Hexagonal architecture with Golang (part 1)

·

11 min read

Featured on Hashnode

Introduction

Before, when I began to develop a new project, I often thought about which programming language and technology stack should be used in the project. Then, I crazily develop the features of the project and try hard to release it. After a while, the features are expanded or need to change technology, so I took a lot of time to update my code structure. The main cause is that the code of the project is unstructured and it is coupled between components.

Therefore, I realize that organizing code structure is important for a project. It determines whether you can easily develop the project when there is a change or if it is easy for many members to work on a project.

Currently, there are many ways to organize code structure for a project such as Flat structure, Layer structure, Modular structure, ... It is ok but I was impressed by the Hexagonal architecture or ports and adapters when I first read about it.

Hexagonal architecture concept

Hexagonal architecture is introduced by Alistair Cockburn and written on his blog in 2005. It is built based on two main patterns:

Hexagonal represents business logic. It does not care about your framework development, your technology stack and your language code. It helps you define decoupled code structure, isolated with external components and easy-to-run tests.

The architecture includes several important components, I will present and explain them through simple examples. I have a managing user software project that has a feature to sign up for a new user. Let's say I already have a GUI (Web frontend) and my task is developing a backend service. The following sequence diagram:

We have four components in this diagram: User, Web UI, User service and Database. User service provides HTPT POST API /signup for Web UI. Internal User service includes three components:

  • REST Controller: receives requests from Web UI through APIs. Then, it forwards them to a handler to do business logic.

  • Handler: validate requests and do logic. If business data needs to be stored in the database, it forwards data to the Repository. In this example, it stores a new user when a user signs up.

  • Repository: convert business data to data objects and send them to the database through database protocol. In this example, it sends an INSERT statement to a database when creating a new user.

Now, I will convert the above diagram to the hexagonal architecture.

Components of the User service are put on the edges of the hexagonal. It is easy to see that they are separated from each other. Because they have different roles, they should not be dependent on each other. When a component is changed, other components should not be affected. How can you do that?

I will continue to convert the above hexagonal architecture of User service to standard hexagonal architecture.

Let's learn the main components of Hexagonal architecture.

Port

Application is your business logic as core architecture. It defines Ports (interface) to communicate with external components (specific technology / outside component) such as Web UI, Database, .... Business logic only knows these ports and ports will represent specific use cases of the logic. There are two port types:

  • Primary Port / Driver Port: provide features of the application to the outside component. We can call them as the use case boundary of the application or the API of the application. Example: it provides a sign-up feature called sign-up handler.

  • Secondary Port / Driven Port: provide features of outside components to the application that implements business logic. Example: it provides a creating record feature in a database called a sign-up repository.

Adapter

The component to connect ports with external components is an adapter. There are two adapter types:

  • Primary adapter / Driver adapter: use the port interface or trigger the feature of the port to perform the business logic. It also converts requests from outside components to requests of the application.

    • For the above example, the REST controller is the primary adapter. It defines API POST /signup for Web UI to call. When Web UI sends a POST request to API /signup, the REST controller converts the request and sends it to the business logic through the sign-up handler (primary port).

  • Secondary adapter / Driven adapter: implement the secondary port. The application uses it to communicate with outside components. It converts the request of the business logic to requests of outside technology components.

    • For the above example, the sign-up logic needs to create a new record in a database. It calls the repository (secondary port) to create a new record, and the MySQL repository converts this request to SQL statement "INSERT INTO user (id, username, displayname, created_at) VALUES (1, "taiptht", "ajpham", 1379782800000) and send it to the MySQL database.

A port can have many different adapters. We can implement a PostgreSQLRepository (secondary adapter) that implements Repository (secondary port) or implement gRPC Controller (primary adapter) that provides sign-up API. That means, an application only cares about a port interface and the port uses any adapter to obtain a business target. This shows the flexibility and separation between the application and the technology components.

Actor

I refer to two components: Web UI (Front end) and MySQL database. They are called actors. There are two actor types:

  • Primary actor / Driver: use features of the application or interact with the application to achieve a business target such as Web UI, mobile application, human (customer), external applications, ...

  • Secondary actor / Driven: provide feature to application use or triggered by application to implement the business logic. There are two driven types:

    • Repository: provide read and write/send features for the application such as database, cache, queue, ...

    • Recipient: provide send feature for the application such as mail server, SMTP server, queue, ...

I introduced the concept of components in the hexagonal architecture. The theory is like that, we can apply it in the project, please see the next section.

How to build a hexagonal code structure?

Dependency pattern

I mentioned two patterns that hexagonal use: Adapter Pattern and Dependency Injection. They help the architecture that is flexible and decoupled between components. How are they used in architecture? Let's start from the driver's side.

On this side, there are four components related together: Actor, Adapter, Port and Application (business logic). This is important that outside components only communicate with an application through ports and adapters. And the application does not care who uses its feature. Back to the sign-up feature of the above example, we have the following model implementation:

A SignUpController class uses a SignUpHandler interface to perform a sign-up feature. SignUpService is an implementation of the SignUpHandler. It implements the sign-up function that is defined in the SignUpHandler interface. A code dependency appears between the SignUpController class and SignUpHandler interface. This is a way to implement a primary adapter and a primary port for the application.

Next, on the driven side, we also have four components related together: Actor, Adapter, Port and Application (business logic). But we have an inversion role. The application communicates with outside components through ports and adapters.

The SignUpService class uses a Repository interface to create a new record. A MySQLRepository is an instance or implementation of the Repository. It defines a way to create records in a MySQL database. A code dependency appears between the SignUpService class with the Repository interface.

In general, we can realize that there is a dependency inversion in the hexagonal architecture. Can see a bellow model:

Data transfer between components

With the power of dependency pattern, the hexagonal architecture separates components together. How do these components communicate together? Language is a means of human-to-human communication. Components in the code structure communicate with each other by data objects.

An adapter is the communication bridge between external components and an application. Therefore, it has a role that converting data type of object that components can understand.

For example, on the driver side, a SignUpController (primary adapter) receives a request from Web UI, it needs to convert the request to a user input before sending it to a SignUpHandler. When receiving a result from the SignUpHanlder, it also converts the user output to a response of Web UI that Web UI can understand.

We also use this idea for the driven side. A MySQLRepository (adapter) converts data objects between a SignUpService and a MySQL database.

Costly

Converting data objects is quite costly that affects the use of memory and the performance of service. But it helps keep decoupled between components in the service. If your service does not change technology frequently, you can reuse data objects between an adapter and a port component.

For example, a SignUpController (adapter) and a SignUpHandler can use the same SignUpRequest and SignUpResponse. That means, the SignUpController sends a SignUpRequest to the SignUpHandler, then the SignUpHandler returns a SignUpResponse to the SignUpController.

How to write a test with hexagonal architecture?

Testing is an important step in the development cycle of a project. With the available isolation benefit of hexagonal architecture, the code structure of the project is very easy to write tests. An application controls business logic, it is an important role of the project. Therefore, we write tests from this component first.

Test an application component

The application does not care about outside components (Web UI, database...), so we apply a test double for outside components. That means, we have to:

  • Write unit tests to trigger the application from a primary port.

  • Implement a mock instance for a secondary adapter.

When writing unit tests for business logic, you have to know the expected result of these test cases and compare them with an actual result when triggering these unit tests. We need to mock a suitable state for each test case.

For example, you want to write a unit test for the case of a successful sign-up feature of a SignUpService. You need to set up that a MockRepository returns a successful result when a SignUpService calls a Repository to create a new user. Then, you assert this result with the expected result. Otherwise, you also return a duplicate user error for a failed test case.

Test an adapter component

Each component is isolated together, so we also write tests for adapter components that are isolated with business logic. Because adapters communicate with outside components, we need to use third parties to be able to run tests.

Test a primary adapter component

We can use httpexpect (apply HTTP REST controller) to trigger the APIs of a primary adapter. There are two ways to perform a test for a primary adapter.

  • Keep a MockRepository as the above example and assert the result at the primary adapter component.

  • Or implement MockSignUpService and set up a suitable state for each test case.

Test a secondary adapter component

About a secondary adapter, we can use a testcontainer to start a database or cache instead of starting a real database or cache. With Go language, it supports several common databases such as MySQL, PostgreSQL,... and supports cache such as Redis.

Then, we also set up unit tests for the primary adapter. Can see the following model:

Next blog, I will provide detailed code for unit test examples by Golang in Hexagonal architecture.

Advantages and Disadvantages

When reading the above sections, you also realize the advantages and disadvantages of hexagonal architecture for software projects, specifically backend projects.

Advantages

Isolation

I want to mention the isolation feature. It is a highlight that makes a difference in this hexagonal architecture. It separates components in code structure: business logic, outside component and technology stack. Each component only performs its role exactly. Components communicate together through interfaces that help decouple between them. Therefore, your code structure is not changed too much when you add or change business logic or technology stack.

Isolation also helps reduce the risk for your project when you change the technology stack. If changing the technology stack has a problem, you only switch using the old adapter.

Flexibility

The technology engine is an indispensable component. With this architecture, you can easily upgrade technology without updating the core (port component, business logic) of the project. You need to implement a new adapter and switch to use this adapter. Otherwise, when you change a business logic (application), you do not update the code of the adapters or port interface.

Testability

As I present above section, writing tests for an application is very easy to perform. Components are isolated together, so we can write isolation unit tests for each . Besides, we can use the test double mechanism to support testing.

Development and maintainability

We can implement core business logic components before choosing a technology stack. Therefore, we can improve the speed of code implementation.

Each component in the code structure can be assigned by different members of a team and members parallel develop components. The maintainer also easily modifies and adds new logic.

Disadvantages

Complexity

If your project is not large or simple business logic, you have to take quite a lot of time to build components and organize code structure. In this case, you should choose other architecture such as layer architecture for your code structure.

Mapping

As I mentioned in the above section, the cost of mapping data objects should be considered. To create isolation between components, you have to trade-off for this action.

In conclusion

Hexagonal architecture really brought many benefits for developing software progress. It changes the mindset about organizing our code structure. It applies a dependency pattern and adapter pattern to create isolation and flexibility for architecture. These projects are very easy to expand, change, upgrade and test because of the separation of components.

How to implement Hexagonal architecture in the Golang project? See you in the next blog here at "Hexagonal architecture with Golang (part 2)".