Hexagonal Architecture using Golang

IRIS
10 min readApr 6, 2023

--

Overview

There are common questions when we start a new software development project like: How to use architecture patterns in my project?, How to swap any technology used in my project?, How to organize my project directory?

In this article, I want to share an example of hexagonal architecture (Ports & Adapters pattern proposed by Alistair Cockburn in 2005) using Golang based on my personal experience in web development projects for easy scalability and maintenance.

Main ideas about Hexagonal architecture

  1. “Your application never touch the real world” (Alistair Cockburn, 2005)
  2. “Swap real world technologies easily” (Alistair Cockburn, 2005)
Figure 1 .- Hexagonal Architecture Chart

Concepts

Application

  • This section has important aspects for the business or problem that the application seeks to solve.
  • Business logic without reference to any real-world technology, framework, or device.
  • The hexagonal architecture does not say anything about the internal structure in the application (The hexagon) so it can have layers or include patterns like DDD, TDD, etc.

Actors

  • Actors are environments, applications, hardware, or any entity that seeks to interact with the application.
  • There are two types of actors, the actors that drive the application (driver actors ) and the actors that are directed by the application (driven actors).

Ports

  • “It is a conversation reason or a communicate intent towards the application” (Alistair Cockburn, 2005).
  • They are all the calls to functions, all the services offered around a type of conversation.
  • “The name for the ports starts with To do something …” (Alistair Cockburn, 2005) for example To manage users.
  • The ports are interfaces that the application offers to the outside world so that actors can interact with them.
  • The ports belong to the application.
  • There are driver and driven ports, based on the type of actor that looking for to interact with our application.

Adapters

  • Component that allows a specific technology to interact with an application port.
  • A driver adapter uses a driver port interface modifying a specific technology request to an agnostic technology request on a driver port.
  • A driven adapter implements a driven port interface by modifying technology agnostic methods of the driven port into technology specific methods.

Case of study

In this section, we are going to implement these previous concepts to a real project with the following functional requirements:

  • Register a new user and save on database

We start with the following directory structure that holds the application and business logic in the ../core directory with its driver and driven ports that are located inside the ../core directory as indicated hexagonal architecture.

├── /cmd
├── /bootstrap
├── main.go
├── /pkg # external libraries and packages to use in this project
└── /internal
├── /adapters
├── /core
│ ├── /application
│ ├── /domain
│ └── /ports
└── /platform # Database and Web Server actors
├── /server
└── /storage

We will implement actors, adapters and ports two for each one. Our study case manage users.

  • Web driver actor (Echo package)
  • Database driven actor (MySQL)
  • Web driver adapter
  • Database driven adapter
  • Web driver port
  • Database driven port

We will start with inside of your application by defining a data transfer object users for this use case

//Path ./internal/core/application/dto/user_dto.go

package dto

import "time"

type User struct {
ID string `json:"id"`
Name string `json:"name"`
Lastname string `json:"lastname"`
Email string `json:"email"`
Password string `json:"password"`
CreatedAt time.Time `json:"created_at,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
DeletedAt *time.Time `json:"deleted_at,omitempty"`
}

Then, we will write the business logic for the users, validations, and instantiate the initial value of each field.

// Path ./internal/core/domain/user.go

package domain

import (
"errors"
"fmt"
"net/mail"
"time"

"github.com/dany0814/go-hexagonal/pkg/uidgen"
)

var ErrUserConflict = errors.New("user already exists")
var ErrInvalidUserID = errors.New("invalid User ID")
var ErrInvalidUserEmail = errors.New("invalid Email")
var ErrInvalidUserPassword = errors.New("invalid Password")
var ErrEmptyName = errors.New("the field name is required")

// NewUserID function to instantiate the initial value for UserID

type UserID struct {
value string
}

func NewUserID(value string) (UserID, error) {
v, err := uidgen.Parse(value)
if err != nil {
return UserID{}, fmt.Errorf("%w: %s", ErrInvalidUserID, value)
}
return UserID{
value: v,
}, nil
}

func (id UserID) String() string {
return id.value
}

// NewUserEmail function to instantiate the initial value for UserEmail

type UserEmail struct {
value string
}

func NewUserEmail(value string) (UserEmail, error) {
_, err := mail.ParseAddress(value)
if err != nil {
return UserEmail{}, fmt.Errorf("%w: %s", ErrInvalidUserEmail, value)
}
return UserEmail{
value: value,
}, nil
}

func (email UserEmail) String() string {
return email.value
}

// NewUserPassword function to instantiate the initial value for UserPassword

type UserPassword struct {
value string
}

func NewUserPassword(value string) (UserPassword, error) {
if value == "" {
return UserPassword{}, fmt.Errorf("%w: %s", ErrInvalidUserPassword, value)
}
return UserPassword{
value: value,
}, nil
}

func (pass UserPassword) String() string {
return pass.value
}

// NewUserUsername function to instantiate the initial value for UserUsername

// NewUser function to instantiate the initial value for User

type User struct {
ID UserID
Name string
Lastname string
Email UserEmail
Password UserPassword
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
}

func NewUser(userID, name, lastname, email, password string) (User, error) {
idVo, err := NewUserID(userID)
if err != nil {
return User{}, err
}

if name == "" {
return User{}, fmt.Errorf("%w: %s", ErrEmptyName, name)
}

emailVo, err := NewUserEmail(email)
if err != nil {
return User{}, err
}

passwordVo, err := NewUserPassword(password)
if err != nil {
return User{}, err
}

return User{
ID: idVo,
Name: name,
Lastname: lastname,
Email: emailVo,
Password: passwordVo,
}, nil
}

func (u User) UserID() UserID {
return u.UserID()
}

Now, we will create the driver port to the web which is an interface for its implementation that depends on the specific technology, a web framework; in this example is echo/v4.

// Path ./internal/core/ports/driver/user_web.go

package driverport

type UserAPI interface {
SignInHandler() error
}

We will also create the port driven to database

// Path ./internal/core/ports/driven/user_db.go

package drivenport

import (
"context"

"github.com/dany0814/go-hexagonal/internal/core/domain"
)

type UserDB interface {
Create(ctx context.Context, user domain.User) error
}

Finally with close your application, we will add the service that uses the business logic in our domain and calls the methods created in the port driven to our database.

// ./internal/core/application/user_service.go

package application

import (
"context"
"time"

"github.com/dany0814/go-hexagonal/internal/core/application/dto"
"github.com/dany0814/go-hexagonal/internal/core/domain"
outdb "github.com/dany0814/go-hexagonal/internal/core/ports/driven"
"github.com/dany0814/go-hexagonal/pkg/encryption"
"github.com/dany0814/go-hexagonal/pkg/uidgen"
)

type UserService struct {
userDB outdb.UserDB
}

func NewUserService(userDB outdb.UserDB) UserService {
return UserService{
userDB: userDB,
}
}

func (usrv UserService) Register(ctx context.Context, user dto.User) (*dto.User, error) {
id := uidgen.New().New()

newuser, err := domain.NewUser(id, user.Name, user.Lastname, user.Email, user.Password)

if err != nil {
return nil, err
}

pass, err := encryption.HashAndSalt(user.Password)

if err != nil {
return nil, err
}

passencrypted, _ := domain.NewUserPassword(pass)

newuser.Password = passencrypted
newuser.CreatedAt = time.Now()
newuser.UpdatedAt = time.Now()

err = usrv.userDB.Create(ctx, newuser)

if err != nil {
return nil, err
}

user.ID = id
return &user, nil
}

So far, we have been created our application with a driver port and a driven port in order to implement a web driver actor and a database driven actor.

We will create the driver adapter for the web driver actor.

// Path ./internal/adapters/driver/user_handler.go

package driveradapt

import (
"errors"
"net/http"

"github.com/dany0814/go-hexagonal/internal/core/application"
"github.com/dany0814/go-hexagonal/internal/core/application/dto"
"github.com/dany0814/go-hexagonal/internal/core/domain"
"github.com/dany0814/go-hexagonal/pkg/helpers"
"github.com/labstack/echo/v4"
_ "github.com/labstack/echo/v4"
)

type UserHandler struct {
userService application.UserService
Ctx echo.Context
}

func NewUserHandler(usrv application.UserService) UserHandler {
return UserHandler{
userService: usrv,
}
}

func (usrh UserHandler) SignInHandler() error {
ctx := usrh.Ctx
var req dto.User
if err := ctx.Bind(&req); err != nil {
ctx.JSON(http.StatusBadRequest, err.Error())
return nil
}
res, err := usrh.userService.Register(ctx.Request().Context(), req)

if err != nil {
switch {
case errors.Is(err, domain.ErrUserConflict):
ctx.JSON(http.StatusConflict, err.Error())
return nil
default:
ctx.JSON(http.StatusInternalServerError, err.Error())
return nil
}
}
ctx.JSON(http.StatusCreated, helpers.DataResponse(0, "User created", res))
return nil
}

To finish implementing the driver actor, we will create our web server in platform directory that uses a specific technology which is echo/v4.

// Path ./internal/platform/server/server.go

package server

import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"time"

web "github.com/dany0814/go-hexagonal/internal/adapters/driver"
"github.com/dany0814/go-hexagonal/internal/core/application"
"github.com/labstack/echo/v4"
)

type AppService struct {
UserService application.UserService
}

type Server struct {
engine *echo.Echo
httpAddr string
ShutdownTimeout time.Duration
app AppService
}

func NewServer(ctx context.Context, host string, port uint, shutdownTimeout time.Duration, app AppService) (context.Context, Server) {
srv := Server{
engine: echo.New(),
httpAddr: fmt.Sprintf("%s:%d", host, port),
ShutdownTimeout: shutdownTimeout,
app: app,
}
srv.registerRoutes()
return serverContext(ctx), srv
}

func (s *Server) Run(ctx context.Context) error {
log.Println("Server running on", s.httpAddr)
srv := &http.Server{
Addr: s.httpAddr,
Handler: s.engine,
}

go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal("server shut down", err)
}
}()

<-ctx.Done()
ctxShutDown, cancel := context.WithTimeout(context.Background(), s.ShutdownTimeout)
defer cancel()

return srv.Shutdown(ctxShutDown)
}

func serverContext(ctx context.Context) context.Context {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
ctx, cancel := context.WithCancel(ctx)
go func() {
<-c
cancel()
}()

return ctx
}

func (s *Server) registerRoutes() {
// User Routes
uh := web.NewUserHandler(s.app.UserService)
s.engine.POST("/user/sigin", func(c echo.Context) error {
uh.Ctx = c
return uh.SignInHandler()
})
}

We have been finished the implementation of a web driver actor that sends data to our application, which execute some validations and applies its business logic to create a new user.

In the other hand, we need save this user data in our database; so we will create the storage driven actor.

// Path ./internal/platform/storage/mysql/user.go

package mysqldb

import "time"

const (
sqlUserTable = "users"
)

type SqlUser struct {
ID string `db:"user_id"`
Name string `db:"name"`
Lastname string `db:"lastname"`
Email string `db:"email"`
Password string `db:"password"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
DeletedAt *time.Time `db:"deleted_at"`
}
// Path ./internal/platform/storage/mysql/user_repository.go

package mysqldb

import (
"context"
"database/sql"
"fmt"
"time"

"github.com/dany0814/go-hexagonal/internal/core/domain"
"github.com/huandu/go-sqlbuilder"
)

type UserRepository struct {
db *sql.DB
dbTimeout time.Duration
}

// NewUserRepository initializes a MySQL-based implementation of UserRepository.
func NewUserRepository(db *sql.DB, dbTimeout time.Duration) *UserRepository {
return &UserRepository{
db: db,
dbTimeout: dbTimeout,
}
}

// Save implements the adapter userRepository interface.
func (r *UserRepository) Save(ctx context.Context, user domain.User) error {
userSQLStruct := sqlbuilder.NewStruct(new(SqlUser))
query, args := userSQLStruct.InsertInto(sqlUserTable, SqlUser{
ID: user.ID.String(),
Name: user.Name,
Lastname: user.Lastname,
Email: user.Email.String(),
Password: user.Password.String(),
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
DeletedAt: user.DeletedAt,
}).Build()

_, err := r.db.ExecContext(ctx, query, args...)
if err != nil {
return fmt.Errorf("Error trying to persist course on database: %v", err)
}

return nil
}

We did it! Finally, we will set up a client for database and some third-party libraries to this application in ./pkg directory.

// Path ./pkg/config/config.go

package config

import (
"context"
"database/sql"
"fmt"
"time"

_ "github.com/go-sql-driver/mysql"
"github.com/kelseyhightower/envconfig"
)

type config struct {
// Database config
DbUser string `default:"admin"`
DbPass string `default:"admin"`
DbHost string `default:"0.0.0.0"`
DbPort string `default:"3306"`
DbName string `default:"admin"`
DbTimeout time.Duration `default:"10s"`
// Server config
Host string `default:"0.0.0.0"`
Port uint `default:"8080"`
ShutdownTimeout time.Duration `default:"20s"`
}

var Cfg config

func LoadConfig() error {
err := envconfig.Process("IRIS", &Cfg)
if err != nil {
return err
}
return nil
}

func ConfigDb(ctx context.Context) (*sql.DB, error) {
mysqlURI := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true", Cfg.DbUser, Cfg.DbPass, Cfg.DbHost, Cfg.DbPort, Cfg.DbName)
fmt.Println("uri: ", mysqlURI)
db, err := sql.Open("mysql", mysqlURI)
if err != nil {
fmt.Println("Failed database connection")
panic(err)
}

fmt.Println("Successfully Connected to MySQL database")

db.SetConnMaxLifetime(time.Minute * 4)
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(10)

err = db.Ping()
if err != nil {
return nil, err
}
return db, nil
}
// Path ./pkg/encryption/bcrypt.go

package encryption

import (
"golang.org/x/crypto/bcrypt"
)

func ComparePasswords(hashed, plain string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashed), []byte(plain))
return err == nil
}

func HashAndSalt(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost)
if err != nil {
return "", err
}
return string(hash), nil
}
// Path ./pkg/helpers/utils.go

package helpers

// Message api
func Message(code int, message string) map[string]interface{} {
return map[string]interface{}{"code": code, "message": message}
}

// Message api error
func MessageError(code int, err error) map[string]interface{} {
return map[string]interface{}{"code": code, "message": err.Error()}
}

// DataResponse api
func DataResponse(code int, message string, data interface{}) map[string]interface{} {
return map[string]interface{}{"code": code, "message": message, "data": data}
}
// Path ./pkg/uidgen/uidgen.go

package uidgen

import "github.com/google/uuid"

type UIDGen interface {
New() string
}

type uidgen struct{}

func New() UIDGen {
return &uidgen{}
}

func (u uidgen) New() string {
return uuid.New().String()
}

func Parse(value string) (string, error) {
v, err := uuid.Parse(value)
if err != nil {
return "", err
}
return v.String(), nil
}

Moreover, we will call the client setup for database and also call server implementation. We will write this calling in ./cmd directory.

// Path ./cmd/bootstrap/bootstrap.go

package bootstrap

import (
"context"
"fmt"
"log"

database "github.com/dany0814/go-hexagonal/internal/adapters/driven"
"github.com/dany0814/go-hexagonal/internal/core/application"
"github.com/dany0814/go-hexagonal/internal/platform/server"
mysqldb "github.com/dany0814/go-hexagonal/internal/platform/storage/mysql"
"github.com/dany0814/go-hexagonal/pkg/config"
)

func Run() error {

err := config.LoadConfig()
if err != nil {
return err
}
fmt.Println("Web server ready!")

ctx := context.Background()
db, err := config.ConfigDb(ctx)

if err != nil {
log.Fatalf("Database configuration failed: %v", err)
}

userRepository := mysqldb.NewUserRepository(db, config.Cfg.DbTimeout)
userAdapter := database.NewUserAdapter(userRepository)
userService := application.NewUserService(userAdapter)

ctx, srv := server.NewServer(context.Background(), config.Cfg.Host, config.Cfg.Port, config.Cfg.ShutdownTimeout, server.AppService{
UserService: userService,
})

return srv.Run(ctx)
}
// Path ./cmd/main.go

package main

import (
"log"

"github.com/dany0814/go-hexagonal/cmd/bootstrap"
)

func main() {
if err := bootstrap.Run(); err != nil {
log.Fatal(err)
}
}

We finished our application using hexagonal architecture!!! To run app, change directory at ./cmd and execute: go run main.go

You can view the final codebase at:

Authors: dany0814, AngelAlvarado.

Conclusions

  • We need a mysql engine run in 3306 port with DbUser: ”admin”, DbPass: “admin” and DbName: ”admin” setup.
  • The name of the directories is optional and didactic purposes.
  • In server.go file that implement a specific web technology, we look that depends on our adapter and in turn, our adapter depends on our application through the driver port with an interface and a SignInHandler() method. The following dependency schema verifies our correct implementation of the hexagonal philosophy.
    Echo Web Server-> Adapter-> Application (Port)
  • The configurable dependencies pattern offers easily way to swap between elements in the real world. For example, using Gin as a web server or PostgreSQL as a database engine.

References

--

--

IRIS
IRIS

Written by IRIS

We develop digital solutions to Industry, start-ups and non-profit organizations about Internet of Things, Software Development & Artificial Intelligence.