Hexagonal Architectural using Go [In-Depth Tutorial]


Antony Shikubu

GO

Hexagonal Architecture, also known as Ports and Adapters Architecture or Clean Architecture, is a software architecture pattern that emphasizes the separation of concerns between the core business logic of an application and the external systems it interacts with. In this pattern, the core business logic is at the center of the architecture, surrounded by adapters that allow it to interact with the outside world. The adapters can be thought of as the "ports" through which the application communicates with external systems.

 

Hexagonal Architecture Components

Here are the components of the Hexagonal Architecture:

  1. Core Business Logic: This is the central component of the architecture, and it contains the application's core domain logic. This component should be independent of any external systems and should not be affected by changes in the infrastructure or the user interface.
  2. Adapters: These are the components that connect the core business logic to the external systems. They can be thought of as the "ports" through which the application communicates with the outside world. Adapters can take many forms, including APIs, databases, user interfaces, and messaging systems.
  3. Primary Adapter: This is the adapter that handles the application's primary input/output. For example, in a web application, the primary adapter might be an HTTP server that accepts incoming requests and returns responses. The primary adapter is responsible for translating incoming requests into domain-specific operations that can be processed by the core business logic, and translating the responses back into a format that can be understood by the requesting system.
  4. Secondary Adapters: These are the adapters that handle the application's secondary input/output. They can be thought of as "plugins" that provide additional functionality to the application. For example, a secondary adapter might be a database adapter that stores data for the application.
  5. Interfaces: These are the contracts that define the communication between the core business logic and the adapters. They ensure that the adapters provide the necessary functionality to the core business logic, and that the core business logic provides the necessary information to the adapters. Interfaces can be thought of as the "language" that the adapters and the core business logic use to communicate with each other.
  6. Dependencies: These are the external libraries or services that the application depends on. They are managed by the adapters, and should not be directly accessed by the core business logic. This allows the core business logic to remain independent of any specific infrastructure or technology choices.

 

Prerequisites

  1. Go runtime installed and basic Go knowledge
  2. Redis installed
  3. PostgreSQL installed
  4. Code editor (recommended VS Code)
  5. Postman for API calls

 

Application structure

Today, you will learn how to create a messaging backend that allows users to save and read messages. Hexagonal architecture adheres to strict application layout that needs to be implemented. Below is the application layout that you will use. This might look like a lot of work, but it will make sense as we move forward. Go ahead and create the below application structure.

└── Messenger
   ├── cmd
   │   └── main.go
   ├── go.mod
   ├── go.sum
   └── internal
       ├── adapters
       │   ├── handler
       │   │   └── http.go
       │   └── repository
       │       ├── postgres.go
       │       └── redis.go
       └── core
           ├── domain
           │   └── model.go
           ├── ports
           │   └── ports.go
           └── services
               └── services.go

Initialize go module.

go mod init Messenger

This application will use different technologies to demonstrate loose coupling using hexagonal architecture. This application will store messages in two data stores. One will be PostgreSQL database and redis. The API for consuming data will be implemented using the Go Gin webframe. Good news is these technology stacks can be switched when need be.

To start us off, navigate to the domain module of your application.

 

Domain (internal/core/domain/domain.go)

This module hosts the Message model in the internal/core/domain directory.

package domain
type Message struct {
   ID   string `json:"id"`
   Body string `json:"body"`
}

Each message in this application will have an ID and a Body attribute. Next, navigate to the internal/core/ports directory and add application interfaces.

 

Ports (internal/core/ports/ports.go)

Ports module host application contracts/interfaces which define the application business logic. These interfaces will be implemented later by adapters.

package ports


import "Messenger/internal/core/domain"


type MessengerService interface {
   SaveMessage(message domain.Message) error
   ReadMessage(id string) (*domain.Message, error)
   ReadMessages() ([]*domain.Message, error)
}


type MessengerRepository interface {
   SaveMessage(message domain.Message) error
   ReadMessage(id string) (*domain.Message, error)
   ReadMessages() ([]*domain.Message, error)
}

In the ports.go file, we define the MessengerService and MessengerRepository interfaces. These two interfaces have the same methods that need to be implemented. In these contracts/logic, you will be able to Save a message, Read a Message and Read Messages. Next , navigate to the services module.

 

Services  (internal/core/services/services.go)

The services module establishes communication between the core and the outside world. In this file, you define a messaging service that implements the MessengerService in the ports module.

package services


import (
   "Messenger/internal/core/domain"
   "Messenger/internal/core/ports"


   "github.com/google/uuid"
)


type MessengerService struct {
   repo ports.MessengerRepository
}


func NewMessengerService(repo ports.MessengerRepository) *MessengerService {
   return &MessengerService{
       repo: repo,
   }
}


func (m *MessengerService) SaveMessage(message domain.Message) error {
   message.ID = uuid.New().String()
   return m.repo.SaveMessage(message)
}


func (m *MessengerService) ReadMessage(id string) (*domain.Message, error) {
   return m.repo.ReadMessage(id)
}


func (m *MessengerService) ReadMessages() ([]*domain.Message, error) {
   return m.repo.ReadMessages()
}

You start off by defining the MessengerService struct that has a repository as one of its attributes.Each service instance must have a repository attribute to enable you get access to whichever datastore you like. In this case we will be able to work with both Redis and PostgreSQL databases. We define a NewMessengerService() function that takes in a repository as an argument and returns a MessengerService instance.

 

Adapter (internal/adapters)

This folder hosts code that implements the MessengerService and MessengerRepository in the services and ports modules respectively. The adapters have been put into two different modules ,based on their functionality. There are the handler modules that serve HTTP requests from Go Gin web framework and the repository module that hosts PostgreSQL and Redis.

Navigate to the internal/adapters/repository/postgres.go file.

 

Repository  

Repositories are secondary actors, meaning the core initiates an event and the repositories respond back to the core.

 

PostgreSQL Repository (internal/adapters/repository/postgres.go)

In the  internal/adapters/repository/postgres.go , add the below code.

package repository


import (
   "Messenger/internal/core/domain"
   "errors"
   "fmt"


   "github.com/jinzhu/gorm"
   _ "github.com/jinzhu/gorm/dialects/postgres"
)


type MessengerPostgresRepository struct {
   db *gorm.DB
}


func NewMessengerPostgresRepository() *MessengerPostgresRepository {
   host := "localhost"
   port := "5432"
   user := "postgres"
   password := "pass1234"
   dbname := "postgres"


   conn := fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=disable",
       host,
       port,
       user,
       dbname,
       password,
   )


   db, err := gorm.Open("postgres", conn)
   if err != nil {
       panic(err)
   }
   db.AutoMigrate(&domain.Message{})


   return &MessengerPostgresRepository{
       db: db,
   }
}


func (m *MessengerPostgresRepository) SaveMessage(message domain.Message) error {
   req := m.db.Create(&message)
   if req.RowsAffected == 0 {
       return errors.New(fmt.Sprintf("messages not saved: %v", req.Error))
   }
   return nil
}


func (m *MessengerPostgresRepository) ReadMessage(id string) (*domain.Message, error) {
   message := &domain.Message{}
   req := m.db.First(&message, "id = ? ", id)
   if req.RowsAffected == 0 {
       return nil, errors.New("message not found")
   }
   return message, nil
}


func (m *MessengerPostgresRepository) ReadMessages() ([]*domain.Message, error) {
   var messages []*domain.Message
   req := m.db.Find(&messages)
   if req.Error != nil {
       return nil, errors.New(fmt.Sprintf("messages not found: %v", req.Error))
   }
   return messages, nil
}

In this file, you define the MessengerPostgresRepository struct that has the gorm ORM as its attribute. This struct implements the MessengerRepository interface in the ports module, by having the SaveMessage, ReadMessage and ReadMessages methods. You also define the NewMessengerPostgresRepository() function that returns an instance of the MessengerPostgresRepository. Next navigate to the internal/adapters/repository/redis.go file.

 

Redis Repository(internal/adapters/repository/redis.go)

In the internal/adapters/repository/redis.go , add the below code.

package repository


import (
   "Messenger/internal/core/domain"
   "encoding/json"


   "github.com/go-redis/redis/v7"
)


type MessengerRedisRepository struct {
   client *redis.Client
}


func NewMessengerRedisRepository(host string) *MessengerRedisRepository {
   client := redis.NewClient(&redis.Options{
       Addr:     host,
       Password: "",
       DB:       0,
   })
   return &MessengerRedisRepository{
       client: client,
   }
}


func (r *MessengerRedisRepository) SaveMessage(message domain.Message) error {
   json, err := json.Marshal(message)
   if err != nil {
       return err
   }
   r.client.HSet("messages", message.ID, json)
   return nil
}


func (r *MessengerRedisRepository) ReadMessage(id string) (*domain.Message, error) {
   value, err := r.client.HGet("messages", id).Result()
   if err != nil {
       return nil, err
   }
   message := &domain.Message{}
   err = json.Unmarshal([]byte(value), message)
   if err != nil {
       return nil, err
   }
   return message, nil
}


func (r *MessengerRedisRepository) ReadMessages() ([]*domain.Message, error) {
   messages := []*domain.Message{}
   value, err := r.client.HGetAll("messages").Result()
   if err != nil {
       return nil, err
   }


   for _, val := range value {
       message := &domain.Message{}
       err = json.Unmarshal([]byte(val), message)
       if err != nil {
           return nil, err
       }
       messages = append(messages, message)
   }


   return messages, nil


}

In the redis.go file, you define the MessengerRedisRepository struct that implements the MessengerRepository interface in the ports module. It has the SaveMessage, ReadMessage and ReadMessages methods. To get a new instance of the redis repository, you define the NewMessengerRedisRepository function, that takes in the redis host as an argument. It returns a new instance of the MessengerRedisRepository.

 

Handler (internal/adapters/http.go)

The handler module hosts the Gin webframe work that serves HTTP requests from clients. In this case the handler is a primary actor, meaning it initiates events that go to the core of the application. Add the below code in the internal/adapters/http.go file.

package handler


import (
   "Messenger/internal/core/domain"
   "Messenger/internal/core/services"


   "net/http"


   "github.com/gin-gonic/gin"
)


type HTTPHandler struct {
   svc services.MessengerService
}


func NewHTTPHandler(MessengerService services.MessengerService) *HTTPHandler {
   return &HTTPHandler{
       svc: MessengerService,
   }
}


func (h *HTTPHandler) SaveMessage(ctx *gin.Context) {
   var message domain.Message
   if err := ctx.ShouldBindJSON(&message); err != nil {
       ctx.JSON(http.StatusBadRequest, gin.H{
           "Error": err,
       })


       return
   }


   err := h.svc.SaveMessage(message)
   if err != nil {
       ctx.JSON(http.StatusBadRequest, gin.H{
           "error": err,
       })
       return
   }


   ctx.JSON(http.StatusCreated, gin.H{
       "message": "New message created successfully",
   })
}


func (h *HTTPHandler) ReadMessage(ctx *gin.Context) {
   id := ctx.Param("id")
   message, err := h.svc.ReadMessage(id)


   if err != nil {
       ctx.JSON(http.StatusBadRequest, gin.H{
           "error": err.Error(),
       })
       return
   }
   ctx.JSON(http.StatusOK, message)
}


func (h *HTTPHandler) ReadMessages(ctx *gin.Context) {


   messages, err := h.svc.ReadMessages()


   if err != nil {
       ctx.JSON(http.StatusBadRequest, gin.H{
           "error": err.Error(),
       })
       return
   }
   ctx.JSON(http.StatusOK, messages)
}

In the internal/adapters/http.go file, you define the HTTPHandler struct as a service as its attribute, which gives you access to our different repositories via the service. The HTTPHandler type implements both the MessangerService and MessangerRepository interfaces.

At this point , you are almost done. You need to run the application. Next , navigate into the cmd/main.go file.

 

cmd (cmd/main.go)

In the main.go file, we bring everything together. We need the repository, services and the HTTP handler code in one place. Add the below code in the main.go file.

package main


import (
   "Messenger/internal/adapters/handler"
   "Messenger/internal/adapters/repository"
   "Messenger/internal/core/services"
   "flag"
   "fmt"


   "github.com/gin-gonic/gin"
)


var (
   repo        = flag.String("db", "postgres", "Database for storing messages")
   redisHost   = "localhost:6379"
   httpHandler *handler.HTTPHandler
   svc         *services.MessengerService
)


func main() {
   flag.Parse()


   fmt.Printf("Application running using %s\n", *repo)
   switch *repo {
   case "redis":
       store := repository.NewMessengerRedisRepository(redisHost)
       svc = services.NewMessengerService(store)
   default:
       store := repository.NewMessengerPostgresRepository()
       svc = services.NewMessengerService(store)
   }


   InitRoutes()


}


func InitRoutes() {
   router := gin.Default()
   handler := handler.NewHTTPHandler(*svc)
   router.GET("/messages/:id", handler.ReadMessage)
   router.GET("/messages", handler.ReadMessages)
   router.POST("/messages", handler.SaveMessage)
   router.Run(":5000")
}

We start off by importing the repositories, service and handler code. Next we define global variables namely repo, redisHost , httpHandler and svc. Please note that the repo variable is a Go flag string value that will be supplied from the command line. The postgreSQL database will be used by default in case redis string is not supplied in the command line. With this you will be able to choose which datastore you want to use.

In the main() function, you parse the flags using flag.Parse() command. Next, you use a switch statement to help you choose the repository to use and from that we initialize a service using the svc := services.NewMessengerService(store) command.

To run the Gin server, you define the InitRoutes() function , that exposes different API endpoints  and runs the server using the router.Run(“:5000”) command. The server will be exposed at port 5000.

 

Running the application

We will start off by running the server using redis as the data store. Make sure you are at the root of your application i.e /Messenger. In the terminal, add any missing dependencies first by issuing the below command.

go mod tidy

 

Using Redis Database

Next, run the server using the below command.

go run cmd/main.go --db=redis

Output

Application running using redis
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.


[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env:   export GIN_MODE=release
- using code:  gin.SetMode(gin.ReleaseMode)


[GIN-debug] GET    /messages/:id             --> Messenger/internal/adapters/handler.(*HTTPHandler).ReadMessage-fm (3 handlers)
[GIN-debug] GET    /messages                 --> Messenger/internal/adapters/handler.(*HTTPHandler).ReadMessages-fm (3 handlers)
[GIN-debug] POST   /messages                 --> Messenger/internal/adapters/handler.(*HTTPHandler).SaveMessage-fm (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :5000

 

Test Server

Using Postman, save a message, read a message and read all messages.

 

a. Save messages

Using the below image and add up to 5 messages.
Hexagonal Architectural using Go [In-Depth Tutorial]


b. Read all messages

Read all messages from the redis database

Hexagonal Architectural using Go [In-Depth Tutorial]


c. Read a message

Read a single message using message ID

Hexagonal Architectural using Go [In-Depth Tutorial]

Using PostgreSQL

In your terminal, stop the previous server session and start a new one then issue the below command.

$ go run cmd/main.go

Output

Application running using postgres
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.


[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env:   export GIN_MODE=release
- using code:  gin.SetMode(gin.ReleaseMode)


[GIN-debug] GET    /messages/:id             --> Messenger/internal/adapters/handler.(*HTTPHandler).ReadMessage-fm (3 handlers)
[GIN-debug] GET    /messages                 --> Messenger/internal/adapters/handler.(*HTTPHandler).ReadMessages-fm (3 handlers)
[GIN-debug] POST   /messages                 --> Messenger/internal/adapters/handler.(*HTTPHandler).SaveMessage-fm (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :5000

 

Test server

Using postman , make up to 5 post requests to the server just like you did above, this time the message body should contain messages like “PostgreSQL message 1” and so on and so forth.

 

Summary

In this article, you learn how to build a Go Messaging application that makes use of Hexagonal architecture. Hexagonal architectural pattern enables software developers to achieve quality attributes. Hexagonal architecture solves coding challenges such as spaghetti code, slow tests , building applications around frameworks. By solving these challenges, you end up developing testable code, flexible, and technology agnostic applications among other many benefits. In this article , you build an application that uses both redis and postgres as datastore and Gin webframe work for serving HTTP requests. This encourages loose coupling of the application.

 

References

https://cardoai.com/what-is-hexagonal-architecture-should-you-use-it/

 

Views: 378

Antony Shikubu

He is highly skilled software developer with expertise in Python, Golang, and AWS cloud services. Skilled in building scalable solutions, he specializes in Django, Flask, Pandas, and NumPy for web apps and data processing, ensuring robust and maintainable code for diverse projects. You can connect with him on Linkedin.

Can't find what you're searching for? Let us assist you.

Enter your query below, and we'll provide instant results tailored to your needs.

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can send mail to admin@golinuxcloud.com

Thank You for your support!!

Leave a Comment

GoLinuxCloud Logo


We try to offer easy-to-follow guides and tips on various topics such as Linux, Cloud Computing, Programming Languages, Ethical Hacking and much more.

Programming Languages

JavaScript

Python

Golang

Node.js

Java

Laravel