Definitive Guide to Software Architecture with Golang

by MacBobby Chibuzor

.

Updated Sun Sep 03 2023

Definitive Guide to Software Architecture with Golang

As a backend engineer, it is essential to understand the principles of structuring applications and adopting architectural patterns that facilitate scalability, maintainability, and efficient code organization.

This article will delve into structuring Go applications.

Also, we will explore the Hexagonal Architecture, providing insights and best practices that can greatly benefit backend engineers.

Structuring Go Applications

No matter the chosen structure, there are commonly named folders across existing Go projects:

  • cmd: The cmd folder is the main entry point for the application. The directory name matches the name of the application.

  • pkg: The pkg folder contains code that external applications may use. Although there is debate on the usefulness of this folder, pkg is explicit, and being explicit makes understanding crystal clear. I am a proponent of keeping this folder solely due to its clarity.

  • internal: The internal folder contains private code and libraries that external applications cannot access.

  • vendor: The vendor folder contains the application dependencies. The go mod vendor command creates it. It’s usually not committed to a code repository unless you create a library; however, some people feel safer having a backup.

  • api: The API folder typically contains an application’s REST API code. It is also a place for Swagger specification, schema, and protocol definition files.

  • web: The web folder contains specific web assets and application components.

  • configs: The configs folder contains configuration files, including confd or consul-template files.

  • init: The init folder contains system initiation (start) and process management (stop/start) scripts with supervisor configurations.

  • scripts: The scripts folder contains scripts to perform various builds, installations, analyses, and operations. Separating these scripts will help to keep the makefile small and tidy.

  • build: The build folder contains files for packaging and continuous integration. Any cloud, container, or package configurations and scripts for packaging are usually stored under the /build/package folder, and continuous integration files are stored under build/ci.

  • deployments (or deploy): The folder stores configuration and template files related to system and container orchestration.

  • test: There are different ways of storing test files. One method is to keep them all together under a test folder or to keep the test files right alongside the code files. This is a matter of preference.

When it comes to standard architectures for medium and large-scale projects

Understanding Hexagonal Architecture

Hexagonal Architecture is based on separating concerns and dependencies within an application. It achieves this by dividing the application into distinct layers, each with its responsibilities and boundaries. The core principles of Hexagonal Architecture are explained below.

Core Business Logic

This is the heart of the application, representing the business rules and domain-specific logic. It remains independent of any external frameworks or interfaces.

golang core business logicBy rule of thumb, internal and external data sources do not directly affect the application logic. This means you can couple and decouple any data source into the application, but it does not change its overall orientation.

Ports

Ports define the interfaces through which the core business logic interacts with the external world. They are abstractions that define the operations the application can perform.

It is a way for your application to interact with the outside world without knowing what it is interacting with. It is a contract that defines how your application should interact with other systems. For instance, if you want to read and write from a database through your application, you will create Read and Write methods in the application to handle this.

golang portThese two methods will not care where the data is coming from – what the data source is (a database, a file system, a message queue, etc). Interfaces are used mostly for building ports.

golang building blockAdapters

Adapters implement the ports by providing concrete implementations that interact with external systems. They are translators between the core logic and external services, such as databases, web frameworks, or third-party APIs.

An adapter can be considered a converter that takes your application's output and converts it into a format usable by an external source.

If changes are made to the adapter, it does not affect the application and the ports in any way.

golang adaptersThe ports and adapter system can be used for your application's output and inputs. For example, whether an API or a message queue is feeding data into your application, there should be ports that are neutral to the data source but able to handle input.

Input to your application is called the driving side, while the output from your application is called the driven side.

Setting Up the Fintech Web Application

Consider a fintech web application that allows users to manage their finances. The application will have user registration, transaction recording, and balance tracking features.

Project Structure

To implement Hexagonal Architecture in Golang, we'll structure our project as follows:

- cmd/
  - main.go
- internal/
  - application/
    - user.go
    - transaction.go
  - domain/
    - user.go
    - transaction.go
  - ports/
    - user_port.go
    - transaction_port.go
- web/
  - handlers/
    - user_handler.go
    - transaction_handler.go
  1. cmd: This directory contains the entry point of our application, main.go, where we'll wire up the dependencies and start the server.

  2. internal/application: This layer holds the application-specific logic. It includes use cases related to users and transactions.

  3. internal/domain: Here, we define the domain models for users and transactions. These models represent the core business logic and are independent of external factors.

  4. internal/ports: This layer defines the ports (interfaces) the application's core logic uses to interact with the outside world. We'll have separate interfaces for user management and transaction handling.

  5. web/handlers: These HTTP handlers act as the adapters, interacting with the web framework. They implement the interfaces defined in the ports layer.

Implementing Hexagonal Architecture in Golang

Let's walk through the implementation of Hexagonal Architecture in our fintech web application.

1. Defining Ports and Adapters

Start by defining the port interfaces in the internal/ports directory. For example:

// internal/ports/user_port.go
package ports

import "context"
import "your-app/internal/domain"

type UserRepository interface {
    SaveUser(ctx context.Context, user *domain.User) error
    GetUserByID(ctx context.Context, userID string) (*domain.User, error)
    // Other methods...
}

type UserUseCase interface {
    RegisterUser(ctx context.Context, name, email string) error
    GetUserByID(ctx context.Context, userID string) (*domain.User, error)
    // Other methods...
}

Here, we've defined the UserRepository interface for data storage and retrieval and the UserUseCase interface for application-specific use cases.

2. Implementing Adapters

Create adapter implementations for the defined ports:

// internal/application/user.go
package application

import "context"
import "your-app/internal/domain"
import "your-app/internal/ports"

type UserUseCase struct {
    userRepository ports.UserRepository
}

func NewUserUseCase(userRepo ports.UserRepository) *UserUseCase {
    return &UserUseCase{userRepository: userRepo}
}

func (u *UserUseCase) RegisterUser(ctx context.Context, name, email string) error {
    // Business logic for user registration
}

func (u *UserUseCase) GetUserByID(ctx context.Context, userID string) (*domain.User, error) {
    // Business logic for getting a user by ID
}

3. Wiring Up Dependencies

In the cmd/main.go file, wire up the dependencies, and start the server:

// cmd/main.go
package main

import (
    "context"
    "net/http"
    "your-app/internal/application"
    "your-app/internal/ports"
    "your-app/web/handlers"
)

func main() {
    // Initialize your database connection and other dependencies

    userRepo := // Initialize your UserRepository implementation
    userUseCase := application.NewUserUseCase(userRepo)

    http.HandleFunc("/users/register", handlers.NewUserHandler(userUseCase).RegisterUser)
    http.HandleFunc("/users/{userID}", handlers.NewUserHandler(userUseCase).GetUserByID)

    // Start your HTTP server
}

4. Creating HTTP Handlers

Implement HTTP handlers that act as adapters by interacting with the web framework:

// web/handlers/user_handler.go
package handlers

import (
    "net/http"
    "your-app/internal/application"
)

type UserHandler struct {
    userUseCase application.UserUseCase
}

func NewUserHandler(userUseCase application.UserUseCase) *UserHandler {
    return &UserHandler{userUseCase: userUseCase}
}

func (h *UserHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
    // Parse request data and call h.userUseCase.RegisterUser
}

func (h *UserHandler) GetUserByID(w http.ResponseWriter, r *http.Request) {
    // Parse path parameters, call h.userUseCase.GetUserByID, and return response
}

Hexagonal Architecture and Domain-Driven Design

The nature of hexagonal applications is such that it is easy to plug them together to form a honeycomb-like bigger application. In this case, each of your hexagons is just a domain in the bigger application. An entire domain-driven application may be a product codebase with several hexagonal applications for handling files, users, etc. These applications are connected using ports and adapters.

golang hexagonalThese smaller applications act as single working units that can stand alone and have a single responsibility. This shows that hexagonal architecture follows the SOLID principles on a large scale:

  • Single Responsibility

  • Open/Closed

  • Liskov Substitution

  • Interface Segregation

  • Dependency Inversion

A real-world example of a large-scale Golang application structure that uses hexagonal architecture is this crypto-exchange application:

.
├── cmd
│  └── cryptoexchange
│      └── main.go
├── Dockerfile
├── go.mod
├── go.sum
├── internal
│  ├── command
│  │  └── start.go
│  ├── cryptoexchange
│  │  ├── api
│  │  │  ├── handlers.go
│  │  │  └── trade_handler.go
│  │  ├── application
│  │  │  ├── httpService.go
│  │  │  ├── orderbook.go
│  │  │  ├── repository.go
│  │  │  └── service.go
│  │  └── domain
│  │     ├── DOM.go
│  │     └── errors.go
│  ├── database
│  │  └── mysql
│  │     ├── database.go
│  │     └── mysql_database.go
│  ├── runner
│  │  └── start.go
│  ├── server
│  │  └── start.go
│  ├── trading
│  │  ├── api
│  │  │  └── handlers.go
│  │  ├── application
│  │  │  ├── repository.go
│  │  │  └── service.go
│  │  ├── domain
│  │  │  ├── errors.go
│  │  │  └── model.go
│  │  └── test
│  │     ├── repository_test.go
│  │     └── service_test.go
│  ├── user
│  │  ├── loginHandler.go
│  │  ├── model
│  │  │  └── user.go
│  │  └── register_handler.go
│  └── wallet
│     ├── api
│     │  └── handlers.go
│     ├── application
│     │  ├── repository.go
│     │  └── service.go
│     ├── domain
│     │  ├── errors.go
│     │  └── model.go
│     └── test
│        ├── repository_test.go
│        └── service_test.go
├── Makefile
├── pkg
│  └── shared
│     ├── database
│     │  └── connection.go
│     ├── helpers
│     │  ├── merkle
│     │  │  ├── merkle.go
│     │  │  └── merkle_test.go
│     │  ├── response
│     │  │  └── response.go
│     │  └── utils.go
│     ├── logging
│     │  └── logger.go
│     └── utils
│        └── encryption.go
├── push
├── query.sql
├── README.md
├── scripts
│  └── build
└── tests
   └── integration
      ├── orderbook_test.go
      └── repository_test.go

Benefits of Using Hexagonal Architecture

  1. Testability: Hexagonal Architecture promotes testability by easily isolating business logic from external components.

  2. Maintainability: Separating concerns simplifies maintenance and updates, as changes can be made within a specific layer without affecting others.

  3. Scalability: The modularity and clear boundaries enable easy scaling of individual components without affecting the entire application.

  4. Change-tolerant: The architecture's flexibility allows for changes in external systems without altering the core business logic.

Tradeoffs of Using Hexagonal Architecture

  1. Complexity in Code: Implementing Hexagonal Architecture can introduce additional complexity, especially for smaller projects.

  2. Performance: The added layers of abstraction can introduce a slight performance overhead, although modern hardware and optimization techniques often mitigate this concern.

Conclusion

Incorporating Hexagonal Architecture into your backend development toolkit can greatly enhance your applications' maintainability, scalability, and overall quality. By separating concerns and dependencies while promoting modularity and testability, this architectural style empowers backend engineers to build robust and adaptable systems that can evolve with changing requirements and technologies.

Whenever you're ready

There are 4 ways we can help you become a great backend engineer:

The MB Platform

Join 1000+ backend engineers learning backend engineering. Build real-world backend projects, learn from expert-vetted courses and roadmaps, track your learnings and set schedules, and solve backend engineering tasks, exercises, and challenges.

The MB Academy

The “MB Academy” is a 6-month intensive Advanced Backend Engineering BootCamp to produce great backend engineers.

Join Backend Weekly

If you like post like this, you will absolutely enjoy our exclusive weekly newsletter, Sharing exclusive backend engineering resources to help you become a great Backend Engineer.

Get Backend Jobs

Find over 2,000+ Tailored International Remote Backend Jobs or Reach 50,000+ backend engineers on the #1 Backend Engineering Job Board

Topic:

Backend Tips, Every week

Backend Tips, Every week