Clean Architecture template for Golang services
The purpose of the template is to show:
Using the principles of Robert Martin (aka Uncle Bob).
Go-clean-template is created & supported by Evrone.
Local development:
# Postgres, RabbitMQ
$ make compose-up
# Run app with migrations
$ make run
Integration tests (can be run in CI):
# DB, app + migrations, integration tests
$ make compose-up-integration-test
cmd/app/main.go
Configuration and logger initialization. Then the main function "continues" in
internal/app/app.go
.
config
Configuration. First, config.yml
is read, then environment variables overwrite the yaml config if they match.
The config structure is in the config.go
.
The env-required: true
tag obliges you to specify a value (either in yaml, or in environment variables).
For configuration, we chose the cleanenv library. It does not have many stars on GitHub, but is simple and meets all the requirements.
Reading the config from yaml contradicts the ideology of 12 factors, but in practice, it is more convenient than reading the entire config from ENV. It is assumed that default values are in yaml, and security-sensitive variables are defined in ENV.
docs
Swagger documentation. Auto-generated by swag library. You don't need to correct anything by yourself.
integration-test
Integration tests. They are launched as a separate container, next to the application container. It is convenient to test the Rest API using go-hit.
internal/app
There is always one Run function in the app.go
file, which "continues" the main function.
This is where all the main objects are created. Dependency injection occurs through the "New ..." constructors (see Dependency Injection). This technique allows us to layer the application using the Dependency Injection principle. This makes the business logic independent from other layers.
Next, we start the server and wait for signals in select for graceful completion.
If app.go
starts to grow, you can split it into multiple files.
For a large number of injections, wire can be used.
The migrate.go
file is used for database auto migrations.
It is included if an argument with the migrate tag is specified.
For example:
$ go run -tags migrate ./cmd/app
internal/controller
Server handler layer (MVC controllers). The template shows 2 servers:
Server routers are written in the same style:
internal/controller/http
Simple REST versioning.
For v2, we will need to add the http/v2
folder with the same content.
And in the file internal/app
add the line:
handler := gin.New()
v1.NewRouter(handler, t)
v2.NewRouter(handler, t)
Instead of Gin, you can use any other http framework or even the standard net/http
library.
In v1/router.go
and above the handler methods, there are comments for generating swagger documentation using swag.
internal/entity
Entities of business logic (models) can be used in any layer. There can also be methods, for example, for validation.
internal/usecase
Business logic.
Repositories, webapi, rpc, and other business logic structures are injected into business logic structures (see Dependency Injection).
internal/usecase/repo
A repository is an abstract storage (database) that business logic works with.
internal/usecase/webapi
It is an abstract web API that business logic works with. For example, it could be another microservice that business logic accesses via the REST API. The package name changes depending on the purpose.
pkg/rabbitmq
RabbitMQ RPC pattern:
In order to remove the dependence of business logic on external packages, dependency injection is used.
For example, through the New constructor, we inject the dependency into the structure of the business logic.
This makes the business logic independent (and portable).
We can override the implementation of the interface without making changes to the usecase
package.
package usecase
import (
// Nothing!
)
type Repository interface {
Get()
}
type UseCase struct {
repo Repository
}
func New(r Repository) *UseCase{
return &UseCase{
repo: r,
}
}
func (uc *UseCase) Do() {
uc.repo.Get()
}
It will also allow us to do auto-generation of mocks (for example with mockery) and easily write unit tests.
We are not tied to specific implementations in order to always be able to change one component to another. If the new component implements the interface, nothing needs to be changed in the business logic.
Programmers realize the optimal architecture for an application after most of the code has been written.
A good architecture allows decisions to be delayed to as late as possible.
Dependency Inversion (the same one from SOLID) is the principle of dependency inversion. The direction of dependencies goes from the outer layer to the inner layer. Due to this, business logic and entities remain independent from other parts of the system.
So, the application is divided into 2 layers, internal and external:
The inner layer with business logic should be clean. It should:
The business logic doesn't know anything about Postgres or a specific web API. Business logic has an interface for working with an abstract database or abstract web API.
The outer layer has other limitations:
internal/entity
).For example, you need to access the database from HTTP (controller).
Both HTTP and database are in the outer layer, which means they know nothing about each other.
The communication between them is carried out through usecase
(business logic):
HTTP > usecase
usecase > repository (Postgres)
usecase < repository (Postgres)
HTTP < usecase
The symbols > and < show the intersection of layer boundaries through Interfaces. The same is shown in the picture:
Or more complex business logic:
HTTP > usecase
usecase > repository
usecase < repository
usecase > webapi
usecase < webapi
usecase > RPC
usecase < RPC
usecase > repository
usecase < repository
HTTP < usecase
Entities are structures that business logic operates on.
They are located in the internal/entity
folder.
In MVC terms, entities are models.
Use Cases is business logic located in internal/usecase
.
The layer with which business logic directly interacts is usually called the infrastructure layer.
These can be repositories internal/usecase/repo
, external webapi internal/usecase/webapi
, any pkg, and other microservices.
In the template, the infrastructure packages are located inside internal/usecase
.
You can choose how to call the entry points as you wish. The options are:
The classic version of Clean Architecture was designed for building large monolithic applications and has 4 layers.
In the original version, the outer layer is divided into two more, which also have an inversion of dependencies to each other (directed inward) and communicate through interfaces.
The inner layer is also divided into two (with separation of interfaces), in the case of complex logic.
Complex tools can be divided into additional layers. However, you should add layers only if really necessary.
In addition to Clean architecture, Onion architecture and Hexagonal (Ports and adapters) are similar to it. Both are based on the principle of Dependency Inversion. Ports and adapters are very close to Clean Architecture, the differences are mainly in terminology.