In the ever-evolving landscape of software development, maintaining modularity and testability is paramount. One architectural pattern that stands out for achieving these goals is the Hexagonal Architecture, also known as the Ports and Adapters pattern. In this blog post, we’ll explore how to implement this architecture in Go (Golang) through a practical example: a simple User management system.
The Hexagonal Architecture promotes a clear separation of concerns, ensuring that the core business logic remains independent of external systems such as databases, APIs, or user interfaces. This separation not only enhances maintainability but also facilitates testing and adaptability.
Let’s dive into the concepts, structure, and implementation details of this architecture in our Go project.

You can find all the project code sources here https://github.com/techerjeansebastienpro/go-hexa-example
Understanding Hexagonal Architecture
What is Hexagonal Architecture?
Hexagonal Architecture, also known as the Ports and Adapters pattern, is an architectural style that aims to create loosely coupled application components that can be easily connected and disconnected. This architecture promotes a separation of concerns by isolating the core business logic from external systems such as databases, user interfaces, and third-party services. The core idea is to have a central « hexagon » or core, surrounded by various « adapters » that handle the communication with external entities through well-defined « ports. »
In Hexagonal Architecture, the core consists of the application’s domain logic, including entities and use cases. The ports define the interfaces through which the core interacts with the outside world. Adapters are the implementations of these interfaces, providing the necessary code to connect the core to external systems. This structure allows the core to remain independent of the technical details of external systems, making the application more modular, maintainable, and testable.
Benefits and Principles
Benefits:
- Modularity: Hexagonal Architecture encourages the development of self-contained modules that are easier to manage and understand. Each module has a clear responsibility and can be developed and tested in isolation.
- Testability: By isolating the core business logic from external dependencies, Hexagonal Architecture makes it easier to write unit tests. Mocking external systems becomes straightforward, allowing for comprehensive testing of the core logic without requiring the actual systems.
- Flexibility and Maintainability: The clear separation between the core and external systems makes it simpler to replace or upgrade components. For instance, switching from one database to another or changing the user interface framework can be done with minimal impact on the core business logic.
- Independence from Frameworks: Hexagonal Architecture reduces the reliance on specific frameworks or technologies. The core logic remains pure and free from framework-specific code, making it easier to adapt to new frameworks or technological advancements.
- Enhanced Collaboration: This architecture facilitates better collaboration between different teams, such as front-end and back-end developers. Since the core logic is separated from the interfaces, different teams can work on their respective parts without interfering with each other.
Principles:
- Separation of Concerns: Divide the application into distinct sections with clear responsibilities. The core should handle the business logic, while adapters should manage the technical details of interacting with external systems.
- Dependency Inversion: Depend on abstractions (ports) rather than concrete implementations. This principle ensures that the core logic does not directly depend on external systems, but rather on interfaces that can be implemented by any external system.
- Explicit Interfaces: Define clear and explicit interfaces (ports) for communication between the core and external systems. This approach makes the interactions well-defined and predictable.
- Independence from External Systems: Ensure that the core logic is not tightly coupled with any external system. The core should be self-sufficient and able to function without relying on specific external technologies or frameworks.
- Adaptability: Design the system in a way that makes it easy to replace or modify components without affecting the core logic. Adapters should be plug-and-play, allowing for seamless integration with different external systems.
Project Structure
Overview of the directory and file organization
cmd/
api/
api.go
internal/
application/
user_dto.go
user_http_handler.go
domain/
user_entities.go
user_ports.go
user_service.go
infrastructure/
user_repository.go
user_api.go
pkg/
models/
Justification for the chosen structure
Hexagonal Architecture principles don’t impose structure or models. We should apply them according to language limits and paradigms.
This implementation is my interpretation of these principles according to my own experience with this language.
I’m open for advice – do not hesitate to comment this blog post.
Domain Layer Implementation
Defining entities (User
)
internal/domain/user_entities.go
On the business side, we manipulate entities. These objects represent the logic of the business.
package domain
type User struct {
ID string
Email string
}
type FindOneById struct {
ID string
}
type FindOneByEmail struct {
Email string
}
type FindOneRequest struct {
FindOneById
FindOneByEmail
}
type CreateUser struct {
Email string
Password string
}
Creating domain services and interfaces (ports)
internal/domain/user_ports.go
Here, we define the interfaces of our application. That’s what are the resources we can retrieve or actions we can do.
package domain
type UsersInput interface {
GetByID(ID string) (User, error)
CreateOne(request CreateUser) (User, error)
}
type UsersOuput interface {
FindOneById(request FindOneById) (User, error)
FindOneByEmail(request FindOneByEmail) (User, error)
InsertOne(request CreateUser) (User, error)
}
internal/domain/user_service.go
This part aims to group all the business logic. If we have to add control checking or business rules …, we can add them here. It does not depend on any kind of technology and environment.
package domain
import (
"errors"
)
type UserService struct {
output UsersOuput
}
func NewUserService(out UsersOuput) *UserService {
return &UserService{output: out}
}
func (s *UserService) FindOne(request FindOneRequest) (User, error) {
if request.ID != "" {
return s.output.FindOneById(FindOneById{ID: request.ID})
}
if request.Email != "" {
return s.output.FindOneByEmail(FindOneByEmail{Email: request.Email})
}
return User{}, errors.New("invalid request")
}
func (s *UserService) Insert(request CreateUser) (User, error) {
return s.output.InsertOne(request)
}
Infrastructure: Implementing Interfaces
Repository to access data sources
internal/infrastructure/user_repository.go
The repository is an implementation of the Output port UserOutput. As the domain service uses an output via dependency inversion, that implementation can be updated without affecting the business logic. In some cases, automated testing, for example, we can use a mocked repository.
package infrastructure
import (
"context"
"github.com/techerjeansebastienpro/go-hexa-example/internal/domain"
db "github.com/techerjeansebastienpro/go-hexa-example/pkg/models"
)
type UserRepository struct {
prisma *db.PrismaClient
}
func NewUserRepository(prisma *db.PrismaClient) *UserRepository {
return &UserRepository{
prisma: prisma,
}
}
func (r *UserRepository) FindOneById(request domain.FindOneById) (domain.User, error) {
ctx := context.Background()
foundUser, err := r.prisma.User.FindUnique(
db.User.ID.Equals(request.ID),
).Exec(ctx)
return domain.User{
ID: foundUser.ID,
}, err
}
func (r *UserRepository) FindOneByEmail(request domain.FindOneByEmail) (domain.User, error) {
ctx := context.Background()
foundUser, err := r.prisma.User.FindUnique(
db.User.Email.Equals(request.Email),
).Exec(ctx)
return domain.User{
ID: foundUser.ID,
}, err
}
func (r *UserRepository) InsertOne(request domain.CreateUser) (domain.User, error) {
ctx := context.Background()
createdUser, err := r.prisma.User.CreateOne(
db.User.Email.Set(request.Email),
db.User.Password.Set(request.Password),
).Exec(ctx)
return domain.User{
ID: createdUser.ID,
Email: createdUser.Email,
}, err
}
API to expose your service
internal/infrastructure/user_api.go
The UserApi implements an input, UserInput, and uses the domain service. This part of the code does not depend on the environment and can be used by multiple « adapters » such as REST API / GraphQL API / gRPC … . We can mock the API in case of automated tests on those adapters.
package infrastructure
import (
"github.com/techerjeansebastienpro/go-hexa-example/internal/domain"
)
type UserApi struct {
userService domain.UserService
}
func NewUserApi(userService domain.UserService) *UserApi {
return &UserApi{
userService: userService,
}
}
func (a *UserApi) GetByID(ID string) (domain.User, error) {
return a.userService.FindOne(domain.FindOneRequest{
FindOneById: domain.FindOneById{
ID: ID,
},
})
}
func (a *UserApi) CreateOne(request domain.CreateUser) (domain.User, error) {
return a.userService.Insert(domain.CreateUser{
Email: request.Email,
Password: request.Password,
})
}
Application: configure your application
Create the first HTTP Handler
package application
import (
"github.com/gin-gonic/gin"
"github.com/techerjeansebastienpro/go-hexa-example/internal/domain"
)
type UserHttpHandler struct {
userInput domain.UsersInput
app *gin.Engine
}
func NewUserHttpHandler(app *gin.Engine, userInput domain.UsersInput) *UserHttpHandler {
return &UserHttpHandler{
userInput: userInput,
app: app,
}
}
func (u *UserHttpHandler) RegisterRoutes() {
u.app.GET("/users/:id", u.GetByID)
u.app.POST("/users", u.Create)
}
func (u *UserHttpHandler) GetByID(c *gin.Context) {
id := c.Param("id")
user, err := u.userInput.GetByID(id)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(200, user)
}
func (u *UserHttpHandler) Create(c *gin.Context) {
var createUser domain.CreateUser
if err := c.ShouldBindJSON(&createUser); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
user, err := u.userInput.CreateOne(createUser)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(201, &UserDTO{
ID: user.ID,
Email: user.Email,
})
}
package application
type UserDTO struct {
ID string `json:"id"`
Email string `json:"email"`
}
Complete Example
Putting it all together in api.go
cmd/api/api.go
The program needs an entry point to bootstrap services and core systems. We instantiate all the services we need to run a specific HTTP service to handle external requests.
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
"github.com/techerjeansebastienpro/go-hexa-example/internal/application"
"github.com/techerjeansebastienpro/go-hexa-example/internal/domain"
"github.com/techerjeansebastienpro/go-hexa-example/internal/infrastructure"
db "github.com/techerjeansebastienpro/go-hexa-example/pkg/models"
)
func main() {
envConfig()
fmt.Println(viper.GetString("DATABASE_URL"))
prismaClient := db.NewClient(
db.WithDatasourceURL(viper.GetString("DATABASE_URL")),
)
if err := prismaClient.Prisma.Connect(); err != nil {
panic(err)
}
defer func() {
if err := prismaClient.Prisma.Disconnect(); err != nil {
panic(err)
}
}()
userService := domain.NewUserService(infrastructure.NewUserRepository((prismaClient)))
api := infrastructure.NewUserApi(*userService)
app := gin.New()
application.NewUserHttpHandler(app, api).RegisterRoutes()
app.Run(":8080")
}
func envConfig() {
viper.SetConfigFile(".env")
viper.ReadInConfig()
}
Explanation of the overall flow
This article provides an implementation overview, illustrating how the Hexagonal Architecture principles are applied in our Go project. Hexagonal Architecture is based on the principles of modularity, separation of concerns, and independence from external systems, ensuring that the core business logic remains isolated from technical details. While Go is a powerful and efficient language for implementing these principles, it does have some limitations, particularly in its support for generics. The limited generics in Go can restrict the ability to encapsulate certain functionalities, which might be more seamlessly achieved in languages with more advanced generic capabilities, such as Java or C#. Nonetheless, Go’s simplicity and strong typing make it a suitable choice for many applications, providing a clear and maintainable structure that adheres to the core concepts of Hexagonal Architecture.
Conclusion
In this blog post, we explored the implementation of Hexagonal Architecture in a Go project through a user management system. We started by understanding the core principles and benefits of this architectural pattern. Then, we delved into the detailed structure of our project, from defining the domain layer to implementing adapters and configuring the infrastructure. By maintaining a clear separation of concerns, Hexagonal Architecture not only enhances the modularity and testability of applications but also makes them more adaptable to changes. Whether you’re working on a small project or a complex system, this architecture can provide a robust foundation. We encourage you to experiment with this architecture in your projects and experience the benefits firsthand. Feel free to share your thoughts and questions in the comments below – we’d love to hear from you! Happy coding!