Neal Ford & Mark Richards, in their book *Fundamentals of Software Architecture: An Engineering Approach, *came up with four dimensions that make up software architecture. They are often referred as the “Pillars of Software Architecture”:
Common challenges in building software systems
What is Hexagonal Architecture?
Hexagonal Architecture, also known as Ports and Adapters Architecture or Clean Architecture, is a software architecture pattern that promotes loose coupling between the application core (business logic) and external components such as user interface, database, and external services.
In Hexagonal Architecture, the core of the application is isolated from external components and is instead accessed through a set of well-defined interfaces or ports. Adapters are then used to implement the required interfaces and integrate with the external components.
By separating the business logic from the external components, Hexagonal Architecture makes it easier to test and maintain the core of the application, and to replace or update external components without affecting the business logic.
Hexagonal Architecture Components
Here are the components of the Hexagonal Architecture:
Core Business Logic:
The Core Business Logic is responsible for the main functionality of the application. This component represents the heart of the application and should be designed to be independent of any external dependencies. In Hexagonal Architecture, the Core Business Logic is implemented as a set of use cases that encapsulate the behavior of the application.
For example, if we are building a banking application, the Core Business Logic would include use cases such as creating an account, transferring funds, and checking account balance.
Adapters:
The Adapters are responsible for connecting the Core Business Logic to the external world. Adapters can be of two types: Primary and Secondary.
Primary Adapter (left side of the hexagon):
The Primary Adapter is responsible for handling incoming requests from the external world and sending them to the Core Business Logic. In Hexagonal Architecture, the Primary Adapter is typically an HTTP server, which receives HTTP requests from clients and converts them into requests that can be understood by the Core Business Logic.
For example, in a banking application, the Primary Adapter would be an HTTP server that listens for incoming requests from clients, such as transferring funds or checking account balances, and then converts them into use cases that can be understood by the Core Business Logic.
Secondary Adapters (right side of the hexagon):
The Secondary Adapters are responsible for interfacing with external dependencies that the Core Business Logic relies on. These dependencies can be databases, message queues, or third-party APIs. Secondary Adapters implement the ports defined by the Core Business Logic.
For example, in a banking application, the Secondary Adapters would include database adapters that interface with the Core Business Logic to store and retrieve data about accounts, transactions, and other related information.
Interfaces:
In software architecture, an interface refers to a contract or an agreement between two software components. It defines a set of rules or protocols that a component must follow in order to communicate with another component.
In the context of hexagonal architecture, interfaces play a critical role as they define the boundaries of the core business logic and the adapters. The core business logic only interacts with the adapters through their interfaces. This allows for easy replacement of adapters without affecting the core business logic.
For example, let’s say you have an online shopping application that needs to process payments. You can define an interface for the payment gateway adapter, which outlines the methods that the core business logic can use to interact with the payment gateway. You can then have multiple payment gateway adapters that implement this interface, such as PayPal, Stripe, and Braintree. The core business logic only interacts with the payment gateway adapters through their defined interface, allowing for easy replacement or addition of payment gateways without affecting the core business logic.
Dependencies:
Dependencies are external resources that the application relies on, such as databases, message queues, or third-party APIs. In Hexagonal Architecture, dependencies are abstracted away from the Core Business Logic and encapsulated in the Secondary Adapters.
For example, a banking application might rely on a third-party API to validate account numbers or a message queue to process transactions. These dependencies would be encapsulated in Secondary Adapters, which would implement the necessary ports to interface with the Core Business Logic.
Why a Hexagon?
Alistair Cockburn is frequently asked whether the hexagon or the number “six” has a particular meaning. His answer to this question is: “No.” He wanted to use a shape that no one had used before. Squares are used everywhere, and pentagons are hard to draw. So it became a hexagon.
The hexagon is also great for drawing in two primary ports on the left and two secondary ports on the right. Cockburn says he’s never encountered a project that required more than four ports for its schematic representation.
Application structure
Today, let’s dive into 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.
Source Code:
Hexagonal-Architecture: Hexagonal Architecture with PostgreSQL, Redis and Go
An Advanced Implementation of Hexagonal Architecture:
Be sure to only dive into the advanced implementation after you understand the first source code.
└── 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
The directory structure follows the basic principles of Hexagonal Architecture by separating the business logic from the infrastructure and presentation layers.
The internal package contains the core business logic, with subdirectories for the domain model, ports (interfaces that allow the application to interact with external systems), and services.
The adapters package contains the infrastructure code that adapts the application to external systems. In this example, there are two subdirectories, handler for HTTP server handlers, and repository for database access code.
The cmd directory contains the main entry point for the application, and the go.mod and go.sum files are used to manage dependencies.
However, it’s worth noting that the suitability of the architecture depends on the specific requirements of the application being built.
Initialize go module:
go mod init github.com/LordMoMA/Hexagonal-Architecture
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 off, navigate to the domain module of your application.
Domain (internal/core/domain/model.go)
As in the hexagonal architecture, business logic is at the center of clean architecture. Around it are the interface adapters, which connect the core with the user interface, the database, and other external components.
The core only knows the interfaces of the adapters but knows nothing about their concrete implementations and the components behind them.
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 theinternal/core/ports directory and add application interfaces.
Ports (internal/core/ports/ports.go)
The business logic knows only these ports; all its use cases are implemented exclusively against the specifications of the ports. It is irrelevant for the business logic which technical details might be behind these ports.
Ports module host application contracts/interfaces which define the application business logic. These interfaces will be implemented later by adapters.
package ports
import "github.com/LordMoMA/Hexagonal-Architecture/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 toSave 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 (
"github.com/LordMoMA/Hexagonal-Architecture/internal/core/domain"
"github.com/LordMoMA/Hexagonal-Architecture/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()
}
We started 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)
Several adapters can be connected to one port. For example, a user interface adapter and a REST adapter can both be connected to the port to control the application. And a port for sending notifications could have an email adapter, an SMS adapter, and a WhatsApp adapter connected to it.
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 (
"errors"
"fmt"
"github.com/LordMoMA/Hexagonal-Architecture/internal/core/domain"
"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, we use the GORM ORM in to interact with a PostgreSQL database.
The MessengerPostgresRepository struct contains a db field of type *gorm.DB, which is the database connection object returned by calling gorm.Open and connecting to a PostgreSQL database with the provided connection string.
The NewMessengerPostgresRepository function returns a new instance of MessengerPostgresRepository initialized with a connection to the PostgreSQL database.
The SaveMessage, ReadMessage, and ReadMessages methods define operations on the database through the db object. For example, SaveMessage takes a domain.Message struct as input and inserts it into the database using the Create method of the db object. Similarly, ReadMessage retrieves a message with the given id from the database using the First method of the db object.
GORM ORM can be used to interact with a PostgreSQL database in a Go program in a clean and efficient manner.
A Little Knowledge:
GORM is an Object-Relational Mapping (ORM) library for Go. It provides a convenient way to interact with databases by mapping database tables to Go structs and vice versa. GORM supports several SQL databases, including MySQL, PostgreSQL, SQLite, and SQL Server.
In the above code example, message is a value of type domain.Message. If we pass it as m.db.Create(message), it would pass the copy of message to the Create() method, and any changes made to the copy would not reflect in the original message variable.
Therefore, we use &message to pass a pointer to the original message variable, which allows Create() method to modify the original message variable directly.
In Go, when a struct is passed to a function as an argument, a copy of the struct is created. When a pointer to a struct is passed as an argument, the function receives the memory address of the original struct, which allows the function to directly modify the original struct.
Next let’s 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 (
"encoding/json"
"github.com/LordMoMA/Hexagonal-Architecture/internal/core/domain"
"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, it has the same logic with the postgres.go file except for some syntax difference. We defined 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, we define the NewMessengerRedisRepository function, that takes in the redis host as an argument. It returns a new instance of the MessengerRedisRepository.
Note that in Redis, HSET and SET are two different commands that operate on different types of data structures.
HSET is used to set the value of a field for a specific hash in Redis. Hashes are maps of key-value pairs in Redis, where each hash is a collection of field-value pairs. HSET command sets the value of a field in a hash to a specified value.
SET is used to set the value of a key in Redis. Keys are like string variables in Redis, where each key has a corresponding value. SET command sets the value of a key to a specified value.
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 (
"net/http"
"github.com/LordMoMA/Hexagonal-Architecture/internal/core/domain"
"github.com/LordMoMA/Hexagonal-Architecture/internal/core/services"
"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, we define the HTTPHandler struct as a service as its attribute, which gives us 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 (
"flag"
"fmt"
"github.com/LordMoMA/Hexagonal-Architecture/internal/adapters/handler"
"github.com/LordMoMA/Hexagonal-Architecture/internal/adapters/repository"
"github.com/LordMoMA/Hexagonal-Architecture/internal/core/services"
"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 started 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, we 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.
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 --> github.com/LordMoMA/Hexagonal-Architecture/internal/adapters/handler.(*HTTPHandler).ReadMessage-fm (3 handlers)
[GIN-debug] GET /messages --> github.com/LordMoMA/Hexagonal-Architecture/internal/adapters/handler.(*HTTPHandler).ReadMessages-fm (3 handlers)
[GIN-debug] POST /messages --> github.com/LordMoMA/Hexagonal-Architecture/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
Source Code:
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
If you encounter the following after sending GET to localhost:5000/messages:
Status: 400 Bad Request
{
"error": "dial tcp [::1]:6379: connect: connection refused"
}
Check if Redis server is running: You can use the following command to check if Redis server is running on your local machine:
$ redis-cli ping
PONG
If the Redis server is running, it should return “PONG”. If it’s not running, you can start it using the following command:
$ redis-server
Output:
7700:C 03 May 2023 03:50:32.702 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
7700:C 03 May 2023 03:50:32.702 # Redis version=7.0.9, bits=64, commit=00000000, modified=0, pid=7700, just started
7700:C 03 May 2023 03:50:32.702 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
7700:M 03 May 2023 03:50:32.703 * monotonic clock: POSIX clock_gettime
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 7.0.9 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in standalone mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 6379
| `-._ `._ / _.-' | PID: 7700
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | https://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
7700:M 03 May 2023 03:50:32.705 # WARNING: The TCP backlog setting of 511 cannot be enforced because kern.ipc.somaxconn is set to the lower value of 128.
7700:M 03 May 2023 03:50:32.705 # Server initialized
7700:M 03 May 2023 03:50:32.705 * Ready to accept connections
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 --> github.com/LordMoMA/Hexagonal-Architecture/internal/adapters/handler.(*HTTPHandler).ReadMessage-fm (3 handlers)
[GIN-debug] GET /messages --> github.com/LordMoMA/Hexagonal-Architecture/internal/adapters/handler.(*HTTPHandler).ReadMessages-fm (3 handlers)
[GIN-debug] POST /messages --> github.com/LordMoMA/Hexagonal-Architecture/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
Source Code:
Hexagonal-Architecture: Hexagonal Architecture with PostgreSQL, Redis and Go
Test server
Using postman , make up to 5 post requests to the server just like we did above, this time the message body should contain messages like “PostgreSQL message 1” and so on and so forth.
└── 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
Summary
In this article, we went through 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, we ended up developing testable code, flexible, and technology agnostic applications among other many benefits. In this article , we built 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.
There are also some criticism on this architecture:
The term “hexagonal” implies that there are 6 parts to the concept, whereas there are only 4 key areas. The term’s usage comes from the graphical conventions that shows the application component like a hexagonal cell. The purpose was not to suggest that there would be six borders/ports, but to leave enough space to represent the different interfaces needed between the component and the external world. According to Martin Fowler, the hexagonal architecture has the benefit of using similarities between presentation layer and data source layer to create symmetric components made of a core surrounded by interfaces, but with the drawback of hiding the inherent asymmetry between a service provider and a service consumer that would better be represented as layers.
With the above criticism shown, the approach to designing a complex business application is usually a combination of domain-driven design, microservices, and hexagonal architecture:
Using strategic design to plan the core domain, subdomains, and bounded contexts.
Breaking down a bounded context into one or more microservices. A microservice can contain one or more aggregates — but also the complete bounded context, as long as this is not too large (and the application becomes a monolith instead of a microservice).
Implementation of the application hexagon according to tactical design, i.e., with entities, value objects, aggregates, services, etc.
Hexagonal architecture or “ports and adapters” (alternatively clean architecture or onion architecture) are an architectural pattern that eliminates the problems of traditional layered architecture (leakage of technical details to other layers, poor testability) and allows decisions about technical details (e.g., the database used) to be deferred and changed without having to adapt the core of the application.
Business code resides in the application core, remains independent of technical code in the infrastructure, and can be developed and tested in isolation.
All source code dependencies point exclusively in the direction of the core. The dependency inversion principle is applied when control flows in the opposite direction, i.e., from the core to the infrastructure (e.g., to the database).
The hexagonal design pattern requires additional effort and is particularly suitable for complex business applications with an expected lifetime of several years to decades.
Reference:
Hexagonal architecture: What is it and why should you use it? redis/go-redis: Redis Go client Hexagonal architecture (software) - Wikipedia