Implementing hexagonal architecture in Go

Andy Beak
8 min readJun 16, 2023

Introduction

In this article we’re going to first take a look at understanding the goals and principals of Hexagonal Architecture and then implement a simple HTTP API endpoint in Go.

We aim to conceptualise using ports and adapters as a means to decouple your core logic from the services that interact with it.

The code is available on Github.

Theory of hexagonal architecture

Hexagonal architecture was first defined by Alistair Cockburn in 2005. The name “hexagonal” comes from the original diagrams which had six sided figures. The number of sides in the diagram is arbitrary and Cockburn later renamed it as the “Ports and Adapter pattern”.

Hexagonal architecture enables loose coupling between the components of your application. It works well with a module pattern which makes it suitable for very large monolith projects.

The result of using hexagonal architecture is that business logic is encapsulated into a “core” package that is decoupled from the rest of your project.

Actors

In hexagonal architecture we call anything that the core interacts with an “actor”.

A primary, or driving, actor is one which will invoke services on the core. A human viewing a webpage could be a driving actor in a web application, for example.

A secondary, or driven, actor is one which the core will invoke in order to perform its function. For example a database server could be an example of a driven actor.

Ports

Ports” are the interfaces that describe how communication needs to occur between the core and the actors. A primary port is one which connects to a primary actor, and likewise a secondary port connects to a driven actor.

Therefore, primary ports describe functionality that the core must implement while secondary ports describe the interface to services the core will use.

Arrows show who initiates a request

Ports belong to the core. It’s important that the core package does not depend on anything that is not defined in the core; Remember that we are trying to decouple our business logic from the rest of our application. Concretely, the core package would use interfaces to describe ports.

Adapters

Adapters” are responsible for transforming communication between the core and the actors external to it. The role of the adapter is to allow components with incompatible code interfaces to collaborate. An adapter for a driver (primary) port will translate an incoming external request into a call on a core service.

The adapter for a driven (secondary) port will transform a call by the core to a request on the actor. For example, the core might ask for “get user id X”, and this could be translated to a MySQL implementation. The core is unaware of the technology implementing the persistence, so we could swap out to a Postgres database without making any changes to the core.

An example implementation in Go

Let’s implement a simple HTTP API that lets us create users and then retrieve them.

This is the tree structure of what we will be implementing:

.
├── helpers obtaining database conection
├── internal
│ ├── repositories secondary adapters
│ ├── core must only depend on things defined in core
│ │ ├── domain my internal domain objects
│ │ ├── ports
│ │ └── usecases
│ └── handlers primary adapters
└── orm the externally defined project domain objects

In a much larger project we could break functionality into modules and place the “internal” directory into the module.

Using the format of the diagram above, this is what our concrete example is going to look like:

It’s important to note that we could have multiple primary adapaters to our core. For example, we could also offer GRPC or CLI as a means to interact with core. Likewise, we could be using multiple or different secondary adapters; For example, using Postgres instead of MySQL, or calling out to other services like an authorization service.

I’m going to use Google Wire to manage my dependency injection, Gorilla mux for routing, and Gorm to simulate a project domain object. Feel free to use whatever libraries you like here; that’s really the point behind hexagonal architecture — your choice of libraries should not make any impact on the core logic.

I’m going to start building from the inside out — defining my core functionality in the ports. This will give me a good idea of where I want to end up.

I need to be able to create a user and retrieve it, so here is the functionality my core needs:

package ports

import (
"context"
"github.com/andybeak/hexagonal-demo/internal/core/domain"
)

// UserUseCase is a primary port that the core must respond to
type UserUseCase interface {
CreateUser(ctx context.Context, name string) (domain.User, error)
GetUserById(ctx context.Context, id string) (domain.User, error)
}

// UserRepository is a secondary port that the core will make calls to
type UserRepository interface {
Save(user domain.User) (domain.User, error)
GetUserById(id string) (domain.User, error)
}

Notice that I’m referencing domain.User . My core cannot know or care about what the project thinks a User is. My core defines its own user and the adapters translate the external world user into something the core knows about.

Let’s implement that core user now quickly:

package domain

import "github.com/google/uuid"

type User struct {
Id string `json:"id"`
Name string `json:"name"`
}

func NewUser(name string) User {
return User{
Id: uuid.New().String(),
Name: name,
}
}

Notice that I’m giving it serialisation hints, but I am not giving it any information about persistence.

My core user struct will only implement domain logic that pertains to my module functionality. I don’t need to worry about how the rest of the project treats users, I’m insulated from external changes.

Contrast it to the Gorm struct that I am using to simulate a project entity:

package orm

import (
"github.com/google/uuid"
"gorm.io/gorm"
"time"
)

type Model struct {
ID uuid.UUID `gorm:"type:char(36);primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt
}

// User can be a project entity, or just a DTO for persistence
type User struct {
Model
// Notice that we include persistence details in the struct that is external to core
Name string `gorm:"type:varchar(32)"`
}

Isolating your core domain object from an ORM struct can help to mitigate some of the drawbacks of ORM. In fact, the reason that I’ve chosen to use Gorm here is to demonstrate that we are freeing ourselves of some of the unhappy consequences of choosing to use ORM.

Let’s look at implementing the usecase for our core logic. This is what the primary adapter will call and so we must implement the primary port.

package usecases

import (
"context"
"github.com/andybeak/hexagonal-demo/internal/core/domain"
"github.com/andybeak/hexagonal-demo/internal/core/ports"
)

func ProvideUserUseCase(
userRepository ports.UserRepository,
) ports.UserUseCase {
return &userUseCase{
userRepository: userRepository,
}
}

// userUseCase implements ports.UserUseCase
type userUseCase struct {
userRepository ports.UserRepository
}

func (u userUseCase) CreateUser(ctx context.Context, name string) (domain.User, error) {
user := domain.NewUser(name)
return u.userRepository.Save(user)
}

func (u userUseCase) GetUserById(ctx context.Context, id string) (domain.User, error) {
return u.userRepository.GetUserById(id)
}

Note the following:

  • We are injecting a repository that implements the secondary port that is defined in our core; The core is unaware of how persistence is implemented.
  • We do not need to (should not) export the userUseCase struct because the primary port ( ports.UserUseCase) is exported
  • We are using our internal domain entity rather than the project user. We can define functions on our domain struct to implement domain logic and we don’t have to rely on all of the functionality defined in the external user; This should make you think about S and I of SOLID.

It’s going to be very simple to implement our repository. Remember that is is a secondary adapter that needs to translate calls from the core into technology specific calls to the persistence mechanism.

package adapters

import (
"github.com/andybeak/hexagonal-demo/internal/core/domain"
"github.com/andybeak/hexagonal-demo/internal/core/ports"
"github.com/andybeak/hexagonal-demo/orm"
"github.com/google/uuid"
"gorm.io/gorm"
)

func ProvideUserRepository(db *gorm.DB) ports.UserRepository {
return &mySQLUserRepository{
db: db,
}
}

// mySQLUserRepository implements ports.UserRepository
type mySQLUserRepository struct {
db *gorm.DB
}

func (u mySQLUserRepository) Save(user domain.User) (domain.User, error) {
ormUser := orm.User{
Model: orm.Model{
ID: uuid.Must(uuid.Parse(user.Id)),
},
Name: user.Name,
}
if err := u.db.Create(&ormUser).Error; err != nil {
return domain.User{}, err
}
return user, nil
}

func (u mySQLUserRepository) GetUserById(id string) (domain.User, error) {
var ormUser orm.User
if err := u.db.First(&ormUser, "id = ?", id).Error; err != nil {
return domain.User{}, err
}
return domain.User{
Id: ormUser.ID.String(),
Name: ormUser.Name,
}, nil
}

Again, we are implementing a port and not exporting the struct that does that the implementation.

Notice how the repository is adapting the internal core user entity into the structure we need to use with the rest of the project.

In this situation we are using an ORM. There are lots of problems with ORM, but at least we have decoupled its opinionated architecture from our core logic. If you’re worried about the expense of ORM, you can avoid using it for a particular repository function and implement raw queries while still taking advantage of it for cheap stuff. So if your project forces you to use ORM because of legacy decisions this approach will let you isolate yourself from the effects.

We’re now able to make calls to our core through the usecase that implements the primary port. We can call out to the database through the repository that implements the secondary port.

Let’s create a webhandler as an example of a way to interact with the core. We could implement GRPC or CLI as well, and they would use the same interface as the HTTP.

The webhandler is a bit long so I’m not going to paste all of it in.

package handlers

import (
"encoding/json"
"errors"
"fmt"
"github.com/andybeak/hexagonal-demo/internal/core/ports"
"github.com/golang/gddo/httputil/header"
"github.com/gorilla/mux"
"io"
"log"
"net/http"
"strings"
)

func ProvideUserHttpHandler(
uuc ports.UserUseCase,
) *UserHttpHandler {
return &UserHttpHandler{
uuc: uuc,
}
}

type UserHttpHandler struct {
uuc ports.UserUseCase
}

func (u *UserHttpHandler) getUserById(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
user, err := u.uuc.GetUserById(ctx, vars["id"])
if err != nil {
writeError(w, http.StatusInternalServerError, "Could not get user by id")
}
writeJson(w, user)
}

What we need to notice here is that the webhandler depends on the primary port.

We inject that handler into the router so that Gorilla can make it available to our webuser:

package handlers

import "github.com/gorilla/mux"

func ProvideRouter(
userHandler *UserHttpHandler,
) *mux.Router {
r := mux.NewRouter()
router := r.PathPrefix("/v1").Subrouter()
router.HandleFunc("/users/{id}", userHandler.getUserById).Methods("GET")
return router
}

Then we wrap this up into an http service that we will inject into our app. I’m not going to paste that in here because it’s not particularly interesting and you can see it in the repository.

I’ve skipped discussing details of how the database connection works. The aim of hexagonal architecture is to make this irrelevant to the core functionality.

In a larger project there will be ways of connecting to your various databases and caches. In this project I’ve placed this into the “helpers” package.

Summary

The simplicity of the project functionality makes it more difficult to see the advantages of hexagonal architecture. If this functionality were just one module in a monolith comprising of hundreds of modules it would be more readily apparent that we’re insulating ourselves from external change.

If somebody changes something in the rest of the project then I just need to adjust my adapters. As long as they’re implementing the ports that my core expects then I can be confident that my core functionality is not affected by external change.

The repository is available at https://github.com/andybeak/hexagonal-demo

--

--