Facebook Linkedin Twitter
Posted Sun Dec 19, 2021 •  Reading time: 9 minutes

Hexagonal Architecture with Golang

I guess, at some point, you’ve already had the following doubt: What is the best way to organize my code in Golang?

One of the market accepted techniques is the Hexagonal Architecture.

Before start to show some code, let’s understand what is this architecture about.

Hexagonal Architecture

The Hexagonal Architecture, proposed by Alistair Cockburn, has a core, where the business logic meets. It is independent of external systems, which makes it easier to run regression tests. The architecture was thought to allow adapters to be plugged into the system, through ports, not affecting or changing the system core.

Motivation

It is usual when doing some code to mix business logic with user interface layer. That might bring problems if there is a need to change from, for example, an API based user interface to a GRPC one. In this case, since the business logic is mixed with the API layer, you will probably need to extract it or reimplement on the GRPC layer.

Another common problem is to mix the business logic implementation with the database access code. As explained before, whether you need to change from a database type to another, you will need to carry the business logic as well.

Structure

The Hexagonal Architecture has four main concepts: core, adapter, ports and actors.

Hexagonal Architecture proposed by Alistair Cockburn

Hexagonal Architecture proposed by Alistair Cockburn

Core

Contains all entities and business logic, it is independent of the application infrastructure and easily testable. The core doesn’t know about any other component other than himself.

Actors

Everything that interacts with the core: databases, other systems, user interface layer and so on. They can be primary or secondary:

  • Primary actors: who start a communication with the core. Example: another application, user interface layer
  • Secondary actores: who expect the core to start communication. Examples: databases, message systems, other applications

Actors and core may speak different languages. You might have a case where an outside application send a http request to execute some logic inside the core, which doesn’t understand what http is about.

The interface between core and actors must be delegated to some entity with understanding on how to convert informations in a structure the core understands, to a structure actors understand.

In order to make this job done, there are the adapters and ports.

Ports

Ports are core belonging interface and define how the communication between actors and core should be addressed.

There are two port types:

  • Primary actors ports: use case definitions the core implements and expose to be consumed by outside actors
  • Secondary actors ports: actions definitions the secondary actor has to implement

Adapters

We can find implementation of the information translation between core and actors here. They are also classified as primary and secondary, following the same idea as ports and actors.

Dependency injection

It is the moment adapters are plugged within the ports. This might happen when the application starts and allows to choose among a in-memory database, if you want to run some tests, or Postgres for production environment.

Coding a ToDo List with Hexagonal Architecture

The next step is to show a ToDo List implementation using the Hexagonal Architecture concepts. We will create APIs to consume from the core, and use MySQL and MongoDB as databases, to tests the adapters implementation.

TL;DR: github.

Folder structure

The project has three main folders: helpers, internal and migrations. Helpers and migrations contains support files, for database connections and table creations, and some transformations.

The internal folder has all the business logic, beside ports and adapters.

https://miro.medium.com/max/523/1*On6-dZXO7uEwP91dQRfYVg.png

The system domain, business logic and ports are defined inside the core folder.

https://miro.medium.com/max/523/1*ewts1do9YjPW6dgKcVULuw.png

The domain contains representations of important entities for the system as ToDo structure.

package domain

import "fmt"

type ToDo struct {
	ID          string
	Title       string
	Description string
}

func NewToDo(id string, title, description string) *ToDo {
	return &ToDo{
		ID:          id,
		Title:       title,
		Description: description,
	}
}

func (t *ToDo) String() string {
	return fmt.Sprintf("%s - %s", t.Title, t.Description)
}

The ports have methods signatures used by adapters. Os this project, we use primary ports

package ports

import (
	"github.com/dexterorion/hexagonal-architecture-mongo-mysql/internal/core/domain"
)

type TodoUseCase interface {
	Get(id string) (*domain.ToDo, error)
	List() ([]domain.ToDo, error)
	Create(title, description string) (*domain.ToDo, error)
}

and secondary ports

package ports

import (
	"github.com/dexterorion/hexagonal-architecture-mongo-mysql/internal/core/domain"
)

type TodoRepository interface {
	Get(id string) (*domain.ToDo, error)
	List() ([]domain.ToDo, error)
	Create(todo *domain.ToDo) (*domain.ToDo, error)
}

The business logic lies inside the usecases folder. They don’t depend on anything other than the business. On the following code, we can see the secondary ports usage.

package usecases

import (
	"github.com/dexterorion/hexagonal-architecture-mongo-mysql/helpers"
	"github.com/dexterorion/hexagonal-architecture-mongo-mysql/helpers/logging"
	"github.com/dexterorion/hexagonal-architecture-mongo-mysql/internal/core/domain"
	"github.com/dexterorion/hexagonal-architecture-mongo-mysql/internal/core/ports"
)

var (
	log = logging.NewLogger()
)

type todoUseCase struct {
	todoRepo ports.TodoRepository
}

func NewToDoUseCase(todoRepo ports.TodoRepository) ports.TodoUseCase {
	return &todoUseCase{
		todoRepo: todoRepo,
	}
}

func (t *todoUseCase) Get(id string) (*domain.ToDo, error) {
	todo, err := t.todoRepo.Get(id)
	if err != nil {
		log.Errorw("Error getting from repo", logging.KeyID, id, logging.KeyErr, err)
		return nil, err
	}

	return todo, nil
}

func (t *todoUseCase) List() ([]domain.ToDo, error) {
	todos, err := t.todoRepo.List()
	if err != nil {
		log.Errorw("Error listing from repo", logging.KeyErr, err)
		return nil, err
	}

	return todos, nil
}

func (t *todoUseCase) Create(title, description string) (*domain.ToDo, error) {
	todo := domain.NewToDo(helpers.RandomUUIDAsString(), title, description)

	_, err := t.todoRepo.Create(todo)
	if err != nil {
		log.Errorw("Error creating from repo", "todo", todo, logging.KeyErr, err)
		return nil, err
	}

	return todo, nil
}

Take a look at the line 15. In order to access any database, we use only the ports definition, therefore the business logic doesn’t have visibility (and must not!) of which database has been used. That structure makes really easy to unplug a MySQL database, and plug a MongoDB if desired, because the developer won’t need to make any deep change on the code.

Ultimately handlers/todo and repositories/todo have the adapters implementation.

Handler (http communication)

package todo


import (
	"github.com/dexterorion/hexagonal-architecture-mongo-mysql/internal/core/ports"
	restful "github.com/emicklei/go-restful/v3"
)


type TodoHandler struct {
	todoUseCase ports.TodoUseCase
}


func NewTodoHandler(todoUseCase ports.TodoUseCase, ws *restful.WebService) *TodoHandler {
	handler := &TodoHandler{
		todoUseCase: todoUseCase,
	}


	ws.Route(ws.GET("/todo/{id}").To(handler.Get).Consumes(restful.MIME_JSON).Produces(restful.MIME_JSON))
	ws.Route(ws.GET("/todo").To(handler.List).Consumes(restful.MIME_JSON).Produces(restful.MIME_JSON))
	ws.Route(ws.POST("/todo").To(handler.Create).Consumes(restful.MIME_JSON).Produces(restful.MIME_JSON))


	return handler
}


func (tdh *TodoHandler) Get(req *restful.Request, resp *restful.Response) {
	id := req.PathParameter("id")


	result, err := tdh.todoUseCase.Get(id)
	if err != nil {
		resp.WriteError(500, err)
		return
	}


	var todo *ToDo = &ToDo{}


	todo.FromDomain(result)
	resp.WriteAsJson(todo)
}


func (tdh *TodoHandler) Create(req *restful.Request, resp *restful.Response) {
	var data = new(ToDo)
	if err := req.ReadEntity(data); err != nil {
		resp.WriteError(500, err)
		return
	}


	result, err := tdh.todoUseCase.Create(data.Title, data.Title)
	if err != nil {
		resp.WriteError(500, err)
		return
	}


	var todo ToDo = ToDo{}
	todo.FromDomain(result)
	resp.WriteAsJson(todo)
}


func (tdh *TodoHandler) List(req *restful.Request, resp *restful.Response) {
	result, err := tdh.todoUseCase.List()
	if err != nil {
		resp.WriteError(500, err)
		return
	}


	var todos ToDoList = ToDoList{}


	todos = todos.FromDomain(result)
	resp.WriteAsJson(todos)
}

We can see the isolation between the http and the business logic layers. Line 11 shows use case injections through ports. That makes the business logic isolated from outside world, becoming easy if we need to change from API based user interface to GRPC for example.

The isolation helps the handlers as well. Whether we need to change the business logic, we won’t need to change anything inside the handler.

MySQL

package todo

import (
	"database/sql"
	"fmt"

	"github.com/dexterorion/hexagonal-architecture-mongo-mysql/internal/core/domain"
	"github.com/dexterorion/hexagonal-architecture-mongo-mysql/internal/core/ports"
)

type toDoMysql struct {
	ID          string
	Title       string
	Description string
}

type toDoListMysql []toDoMysql

func (m *toDoMysql) ToDomain() *domain.ToDo {
	return &domain.ToDo{
		ID:          m.ID,
		Title:       m.Title,
		Description: m.Description,
	}
}
func (m *toDoMysql) FromDomain(todo *domain.ToDo) {
	if m == nil {
		m = &toDoMysql{}
	}

	m.ID = todo.ID
	m.Title = todo.Title
	m.Description = todo.Description
}

func (m toDoListMysql) ToDomain() []domain.ToDo {
	todos := make([]domain.ToDo, len(m))
	for k, td := range m {
		todo := td.ToDomain()
		todos[k] = *todo
	}

	return todos
}

type todoMysqlRepo struct {
	db *sql.DB
}

func NewTodoMysqlRepo(db *sql.DB) ports.TodoRepository {
	return &todoMysqlRepo{
		db: db,
	}
}

func (m *todoMysqlRepo) Get(id string) (*domain.ToDo, error) {
	var todo toDoMysql = toDoMysql{}
	sqsS := fmt.Sprintf("SELECT id, title, description FROM todo WHERE id = '%s'", id)

	result := m.db.QueryRow(sqsS)
	if result.Err() != nil {
		return nil, result.Err()
	}

	if err := result.Scan(&todo.ID, &todo.Title, &todo.Description); err != nil {
		return nil, err
	}

	return todo.ToDomain(), nil
}

func (m *todoMysqlRepo) List() ([]domain.ToDo, error) {
	var todos toDoListMysql
	sqsS := "SELECT id, title, description FROM todo"

	result, err := m.db.Query(sqsS)
	if err != nil {
		return nil, err
	}

	if result.Err() != nil {
		return nil, result.Err()
	}

	for result.Next() {
		todo := toDoMysql{}

		if err := result.Scan(&todo.ID, &todo.Title, &todo.Description); err != nil {
			return nil, err
		}

		todos = append(todos, todo)
	}

	return todos.ToDomain(), nil
}

func (m *todoMysqlRepo) Create(todo *domain.ToDo) (*domain.ToDo, error) {
	sqlS := "INSERT INTO todo (id, title, description) VALUES (?, ?, ?)"

	_, err := m.db.Exec(sqlS, todo.ID, todo.Title, todo.Description)

	if err != nil {
		return nil, err
	}

	return todo, nil
}

Mongo

package todo

import (
	"database/sql"
	"fmt"

	"github.com/dexterorion/hexagonal-architecture-mongo-mysql/internal/core/domain"
	"github.com/dexterorion/hexagonal-architecture-mongo-mysql/internal/core/ports"
)

type toDoMysql struct {
	ID          string
	Title       string
	Description string
}

type toDoListMysql []toDoMysql

func (m *toDoMysql) ToDomain() *domain.ToDo {
	return &domain.ToDo{
		ID:          m.ID,
		Title:       m.Title,
		Description: m.Description,
	}
}
func (m *toDoMysql) FromDomain(todo *domain.ToDo) {
	if m == nil {
		m = &toDoMysql{}
	}

	m.ID = todo.ID
	m.Title = todo.Title
	m.Description = todo.Description
}

func (m toDoListMysql) ToDomain() []domain.ToDo {
	todos := make([]domain.ToDo, len(m))
	for k, td := range m {
		todo := td.ToDomain()
		todos[k] = *todo
	}

	return todos
}

type todoMysqlRepo struct {
	db *sql.DB
}

func NewTodoMysqlRepo(db *sql.DB) ports.TodoRepository {
	return &todoMysqlRepo{
		db: db,
	}
}

func (m *todoMysqlRepo) Get(id string) (*domain.ToDo, error) {
	var todo toDoMysql = toDoMysql{}
	sqsS := fmt.Sprintf("SELECT id, title, description FROM todo WHERE id = '%s'", id)

	result := m.db.QueryRow(sqsS)
	if result.Err() != nil {
		return nil, result.Err()
	}

	if err := result.Scan(&todo.ID, &todo.Title, &todo.Description); err != nil {
		return nil, err
	}

	return todo.ToDomain(), nil
}

func (m *todoMysqlRepo) List() ([]domain.ToDo, error) {
	var todos toDoListMysql
	sqsS := "SELECT id, title, description FROM todo"

	result, err := m.db.Query(sqsS)
	if err != nil {
		return nil, err
	}

	if result.Err() != nil {
		return nil, result.Err()
	}

	for result.Next() {
		todo := toDoMysql{}

		if err := result.Scan(&todo.ID, &todo.Title, &todo.Description); err != nil {
			return nil, err
		}

		todos = append(todos, todo)
	}

	return todos.ToDomain(), nil
}

func (m *todoMysqlRepo) Create(todo *domain.ToDo) (*domain.ToDo, error) {
	sqlS := "INSERT INTO todo (id, title, description) VALUES (?, ?, ?)"

	_, err := m.db.Exec(sqlS, todo.ID, todo.Title, todo.Description)

	if err != nil {
		return nil, err
	}

	return todo, nil
}

Above the communication with Mongo and MySQL databases are implemented. We can observe there is no code other than conversions from the ToDp domain model to ToDo database model, and database actions like create, get and list.

Finally the main file within all initializations.

package main

import (
	"flag"
	"net/http"

	"github.com/dexterorion/hexagonal-architecture-mongo-mysql/helpers"
	"github.com/dexterorion/hexagonal-architecture-mongo-mysql/helpers/logging"
	"github.com/dexterorion/hexagonal-architecture-mongo-mysql/internal/core/ports"
	usecases "github.com/dexterorion/hexagonal-architecture-mongo-mysql/internal/core/usecases"
	handlerTodo "github.com/dexterorion/hexagonal-architecture-mongo-mysql/internal/handlers/todo"
	repoTodo "github.com/dexterorion/hexagonal-architecture-mongo-mysql/internal/repositories/todo"
	restful "github.com/emicklei/go-restful/v3"
	"go.uber.org/zap"
)

var (
	repo    string
	binding string

	log *zap.SugaredLogger = logging.NewLogger()
)

func init() {
	flag.StringVar(&repo, "repo", "mysql", "Mongo or MySql")
	flag.StringVar(&binding, "httpbind", ":8080", "address/port to bind listen socket")

	flag.Parse()
}

func main() {
	var todoRepo ports.TodoRepository
	if repo == "mysql" {
		todoRepo = startMysqlRepo()
	} else {
		todoRepo = startMongoRepo()
	}

	todoUseCase := usecases.NewToDoUseCase(todoRepo)

	ws := new(restful.WebService)
	ws = ws.Path("/api")
	handlerTodo.NewTodoHandler(todoUseCase, ws)
	restful.Add(ws)

	log.Info("Listening...")

	log.Panic(http.ListenAndServe(binding, nil))
}

func startMongoRepo() ports.TodoRepository {
	return repoTodo.NewTodoMongoRepo(helpers.StartMongoDb())
}

func startMysqlRepo() ports.TodoRepository {
	return repoTodo.NewTodoMysqlRepo(helpers.StartMysqlDb())
}

The main file is the orchestrator. The logic inside verifies which repository to use (lines 33-37) and starts the business logic using the chosen repository (line 39). The handler starts and injects the business logic on its context (line 43).

Conclusions

The Hexagonal Architecture brings us tremendous advantages as dependency separation concerns, business logic development focus once we can implement it independently of the other parts of the system, and makes it really easy to change infrastructure as databases changes or user interface layer changes.

However, there are some disadvantages. Smaller services might not be really the case for use the hexagonal architecture because it may take too much time to develop all needed ports and adapters for such small service. Another possible down point is with more layers, more latency in the code. For high performance systems, this might be a problem.