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.
By 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.
These 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.
Adapters
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.
The 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
cmd: This directory contains the entry point of our application,
main.go
, where we'll wire up the dependencies and start the server.internal/application: This layer holds the application-specific logic. It includes use cases related to users and transactions.
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.
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.
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.
These 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
Testability: Hexagonal Architecture promotes testability by easily isolating business logic from external components.
Maintainability: Separating concerns simplifies maintenance and updates, as changes can be made within a specific layer without affecting others.
Scalability: The modularity and clear boundaries enable easy scaling of individual components without affecting the entire application.
Change-tolerant: The architecture's flexibility allows for changes in external systems without altering the core business logic.
Tradeoffs of Using Hexagonal Architecture
Complexity in Code: Implementing Hexagonal Architecture can introduce additional complexity, especially for smaller projects.
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.