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:
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
- Go runtime installed and basic Go knowledge
- Redis installed
- PostgreSQL installed
- Code editor (recommended VS Code)
- 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.
b. Read all messages
Read all messages from the redis database
c. Read a message
Read a single message using message ID
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/