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.
This template implements four types of servers:
The template includes three domains to demonstrate multi-service architecture:
All domains are available across all four transports (REST, gRPC, AMQP RPC, NATS RPC).
The template includes three fully implemented domains, each available across all four transports (REST, gRPC, AMQP RPC, NATS RPC).
Registration, login, and JWT-based authorization.
| Operation | REST | gRPC |
|---|---|---|
| Register | POST /v1/auth/register | AuthService/Register |
| Login | POST /v1/auth/login | AuthService/Login |
| Get profile | GET /v1/user/profile | AuthService/GetProfile |
CRUD operations with a status state machine.
| Operation | REST | gRPC |
|---|---|---|
| Create | POST /v1/tasks | TaskService/CreateTask |
| List | GET /v1/tasks | TaskService/ListTasks |
| Get | GET /v1/tasks/:id | TaskService/GetTask |
| Update | PUT /v1/tasks/:id | TaskService/UpdateTask |
| Transition | PATCH /v1/tasks/:id/status | TaskService/TransitionTask |
| Delete | DELETE /v1/tasks/:id | TaskService/DeleteTask |
todo → in_progress → done (and in_progress → todo)limit/offset and optional status filterText translation via external API with history tracking.
| Operation | REST | gRPC |
|---|---|---|
| Translate | POST /v1/translation/do-translate | TranslationHistoryService/DoTranslate |
| History | GET /v1/translation/history | TranslationHistoryService/ShowHistory |
# Postgres, RabbitMQ, NATS
make compose-up
# Run app with migrations
make run
# DB, app + migrations, integration tests
make compose-up-integration-test
make compose-up-all
Check services:
amqp://guest:guest@127.0.0.1:5672/rpc_clientrpc_servernats://guest:guest@127.0.0.1:4222/rpc_servertcp://grpc.lvh.me:8081 | tcp://127.0.0.1:8081postgres://user:myAwEsOm3pa55@w0rd@127.0.0.1:5432/dbguest / guestguest / guestDistributed tracing is provided by OpenTelemetry. Spans are exported over OTLP/gRPC to a collector — Jaeger in the docker stack.
traceparent + baggage, so a single trace spans all four transports. REST uses
the otelfiber middleware, gRPC uses the
otelgrpc stats handler, and AMQP RPC / NATS RPC carry the
trace context in message headers via custom carriers (pkg/rabbitmq/rmq_rpc/otel_carrier.go,
pkg/nats/nats_rpc/otel_carrier.go).*/tracing.go), so a trace
shows the full path: controller → usecase → repo / webapi.TRACING_ENABLED=false a no-op tracer provider is installed, so instrumentation runs
unconditionally with zero overhead.TRACING_SAMPLE_RATE is a parent-based ratio. Use 1.0 locally; lower it (0.1 / 0.01) in
production./healthz, /metrics, /swagger) are excluded from tracing — only the versioned API groups are
instrumented.Configuration (see .env.example):
| Variable | Default | Description |
|---|---|---|
TRACING_ENABLED | false | Enable OpenTelemetry tracing |
TRACING_OTLP_ENDPOINT | localhost:4317 | OTLP/gRPC collector endpoint |
TRACING_OTLP_INSECURE | true | Disable TLS for the OTLP exporter |
TRACING_SAMPLE_RATE | 0.1 | Parent-based sampling ratio |
cmd/app/main.goConfiguration and logger initialization. Then the main function "continues" in
internal/app/app.go.
configThe twelve-factor app stores config in environment variables (often shortened to env vars or env). Env vars are easy
to change between deploys without changing any code; unlike config files, there is little chance of them being checked
into the code repo accidentally; and unlike custom config files, or other config mechanisms such as Java System
Properties, they are a language- and OS-agnostic standard.
Config: config.go
Example: .env.example
docker-compose.yml uses env variables to configure services.
docsSwagger documentation. Auto-generated by swag library. You don't need to correct anything by yourself.
docs/protoProtobuf files. They are used to generate Go code for gRPC services. The proto files are also used to generate documentation for gRPC services. You don't need to correct anything by yourself.
integration-testIntegration tests. They are launched as a separate container, next to the application container.
internal/appThere 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/controllerServer handler layer (MVC controllers). The template shows 4 servers:
Server routers are written in the same style:
internal/controller/amqp_rpcSimple RPC versioning.
For v2, we will need to add the amqp_rpc/v2 folder with the same content.
And in the file internal/controller/amqp_rpc/router.go add the line:
routes := make(map[string]server.CallHandler)
{
v1.NewRoutes(routes, t, u, tk, j, l)
}
{
v2.NewTranslationRoutes(routes, t, l)
}
internal/controller/grpcSimple gRPC versioning.
For v2, we will need to add the grpc/v2 folder with the same content.
Also add the v2 folder to the proto files in docs/proto.
And in the file internal/controller/grpc/router.go add the line:
{
v1.NewAuthRoutes(app, u, l)
v1.NewTaskRoutes(app, tk, l)
v1.NewTranslationRoutes(app, t, l)
}
{
v2.NewAuthRoutes(app, u, l)
v2.NewTaskRoutes(app, tk, l)
v2.NewTranslationRoutes(app, t, l)
}
reflection.Register(app)
internal/controller/nats_rpcSimple RPC versioning.
For v2, we will need to add the nats_rpc/v2 folder with the same content.
And in the file internal/controller/nats_rpc/router.go add the line:
routes := make(map[string]server.CallHandler)
{
v1.NewRoutes(routes, t, u, tk, j, l)
}
{
v2.NewTranslationRoutes(routes, t, l)
}
internal/controller/restapiSimple REST versioning.
For v2, we will need to add the restapi/v2 folder with the same content.
And in the file internal/controller/restapi/router.go add the line:
apiV1Group := app.Group("/v1")
{
v1.NewRoutes(apiV1Group, t, u, tk, jwtManager, l)
}
apiV2Group := app.Group("/v2")
{
v2.NewRoutes(apiV2Group, t, u, tk, jwtManager, l)
}
Instead of Fiber, you can use any other http framework.
In router.go and above the handler methods, there are comments for generating swagger documentation
using swag.
internal/entityEntities of business logic (models) can be used in any layer. There can also be methods, for example, for validation.
internal/usecaseBusiness logic.
Repositories, webapi, rpc, and other business logic structures are injected into business logic structures (see Dependency Injection).
internal/repo/persistentA repository is an abstract storage (database) that business logic works with.
internal/repo/webapiIt 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/rabbitmqRabbitMQ RPC pattern:
pkg/tracingOpenTelemetry setup: configures a global TracerProvider with an OTLP/gRPC exporter and W3C trace-context + baggage
propagators. When tracing is disabled, a no-op provider is installed so instrumentation code runs unconditionally.
See Observability.
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 go.uber.org/mock) 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 injection. 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

internal/entity folder.
In MVC terms, entities are models.internal/usecase.The layer with which business logic directly interacts is usually called the infrastructure layer.
These can be repositories internal/repo/persistent, external webapi internal/repo/webapi, any pkg, and other
microservices.
In the template, the infrastructure packages are located inside internal/repo.
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.
Overall
Maintained
Code review
Go module
Latest release
Releases this year
(top 24 of 33)
309 followers · starred Mar 2023
608 followers · starred Jun 2022
380 followers · starred May 2022
139 followers · starred May 2022
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.
This template implements four types of servers:
The template includes three domains to demonstrate multi-service architecture:
All domains are available across all four transports (REST, gRPC, AMQP RPC, NATS RPC).
The template includes three fully implemented domains, each available across all four transports (REST, gRPC, AMQP RPC, NATS RPC).
Registration, login, and JWT-based authorization.
| Operation | REST | gRPC |
|---|---|---|
| Register | POST /v1/auth/register | AuthService/Register |
| Login | POST /v1/auth/login | AuthService/Login |
| Get profile | GET /v1/user/profile | AuthService/GetProfile |
CRUD operations with a status state machine.
| Operation | REST | gRPC |
|---|---|---|
| Create | POST /v1/tasks | TaskService/CreateTask |
| List | GET /v1/tasks | TaskService/ListTasks |
| Get | GET /v1/tasks/:id | TaskService/GetTask |
| Update | PUT /v1/tasks/:id | TaskService/UpdateTask |
| Transition | PATCH /v1/tasks/:id/status | TaskService/TransitionTask |
| Delete | DELETE /v1/tasks/:id | TaskService/DeleteTask |
todo → in_progress → done (and in_progress → todo)limit/offset and optional status filterText translation via external API with history tracking.
| Operation | REST | gRPC |
|---|---|---|
| Translate | POST /v1/translation/do-translate | TranslationHistoryService/DoTranslate |
| History | GET /v1/translation/history | TranslationHistoryService/ShowHistory |
# Postgres, RabbitMQ, NATS
make compose-up
# Run app with migrations
make run
# DB, app + migrations, integration tests
make compose-up-integration-test
make compose-up-all
Check services:
amqp://guest:guest@127.0.0.1:5672/rpc_clientrpc_servernats://guest:guest@127.0.0.1:4222/rpc_servertcp://grpc.lvh.me:8081 | tcp://127.0.0.1:8081postgres://user:myAwEsOm3pa55@w0rd@127.0.0.1:5432/dbguest / guestguest / guestDistributed tracing is provided by OpenTelemetry. Spans are exported over OTLP/gRPC to a collector — Jaeger in the docker stack.
traceparent + baggage, so a single trace spans all four transports. REST uses
the otelfiber middleware, gRPC uses the
otelgrpc stats handler, and AMQP RPC / NATS RPC carry the
trace context in message headers via custom carriers (pkg/rabbitmq/rmq_rpc/otel_carrier.go,
pkg/nats/nats_rpc/otel_carrier.go).*/tracing.go), so a trace
shows the full path: controller → usecase → repo / webapi.TRACING_ENABLED=false a no-op tracer provider is installed, so instrumentation runs
unconditionally with zero overhead.TRACING_SAMPLE_RATE is a parent-based ratio. Use 1.0 locally; lower it (0.1 / 0.01) in
production./healthz, /metrics, /swagger) are excluded from tracing — only the versioned API groups are
instrumented.Configuration (see .env.example):
| Variable | Default | Description |
|---|---|---|
TRACING_ENABLED | false | Enable OpenTelemetry tracing |
TRACING_OTLP_ENDPOINT | localhost:4317 | OTLP/gRPC collector endpoint |
TRACING_OTLP_INSECURE | true | Disable TLS for the OTLP exporter |
TRACING_SAMPLE_RATE | 0.1 | Parent-based sampling ratio |
cmd/app/main.goConfiguration and logger initialization. Then the main function "continues" in
internal/app/app.go.
configThe twelve-factor app stores config in environment variables (often shortened to env vars or env). Env vars are easy
to change between deploys without changing any code; unlike config files, there is little chance of them being checked
into the code repo accidentally; and unlike custom config files, or other config mechanisms such as Java System
Properties, they are a language- and OS-agnostic standard.
Config: config.go
Example: .env.example
docker-compose.yml uses env variables to configure services.
docsSwagger documentation. Auto-generated by swag library. You don't need to correct anything by yourself.
docs/protoProtobuf files. They are used to generate Go code for gRPC services. The proto files are also used to generate documentation for gRPC services. You don't need to correct anything by yourself.
integration-testIntegration tests. They are launched as a separate container, next to the application container.
internal/appThere 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/controllerServer handler layer (MVC controllers). The template shows 4 servers:
Server routers are written in the same style:
internal/controller/amqp_rpcSimple RPC versioning.
For v2, we will need to add the amqp_rpc/v2 folder with the same content.
And in the file internal/controller/amqp_rpc/router.go add the line:
routes := make(map[string]server.CallHandler)
{
v1.NewRoutes(routes, t, u, tk, j, l)
}
{
v2.NewTranslationRoutes(routes, t, l)
}
internal/controller/grpcSimple gRPC versioning.
For v2, we will need to add the grpc/v2 folder with the same content.
Also add the v2 folder to the proto files in docs/proto.
And in the file internal/controller/grpc/router.go add the line:
{
v1.NewAuthRoutes(app, u, l)
v1.NewTaskRoutes(app, tk, l)
v1.NewTranslationRoutes(app, t, l)
}
{
v2.NewAuthRoutes(app, u, l)
v2.NewTaskRoutes(app, tk, l)
v2.NewTranslationRoutes(app, t, l)
}
reflection.Register(app)
internal/controller/nats_rpcSimple RPC versioning.
For v2, we will need to add the nats_rpc/v2 folder with the same content.
And in the file internal/controller/nats_rpc/router.go add the line:
routes := make(map[string]server.CallHandler)
{
v1.NewRoutes(routes, t, u, tk, j, l)
}
{
v2.NewTranslationRoutes(routes, t, l)
}
internal/controller/restapiSimple REST versioning.
For v2, we will need to add the restapi/v2 folder with the same content.
And in the file internal/controller/restapi/router.go add the line:
apiV1Group := app.Group("/v1")
{
v1.NewRoutes(apiV1Group, t, u, tk, jwtManager, l)
}
apiV2Group := app.Group("/v2")
{
v2.NewRoutes(apiV2Group, t, u, tk, jwtManager, l)
}
Instead of Fiber, you can use any other http framework.
In router.go and above the handler methods, there are comments for generating swagger documentation
using swag.
internal/entityEntities of business logic (models) can be used in any layer. There can also be methods, for example, for validation.
internal/usecaseBusiness logic.
Repositories, webapi, rpc, and other business logic structures are injected into business logic structures (see Dependency Injection).
internal/repo/persistentA repository is an abstract storage (database) that business logic works with.
internal/repo/webapiIt 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/rabbitmqRabbitMQ RPC pattern:
pkg/tracingOpenTelemetry setup: configures a global TracerProvider with an OTLP/gRPC exporter and W3C trace-context + baggage
propagators. When tracing is disabled, a no-op provider is installed so instrumentation code runs unconditionally.
See Observability.
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 go.uber.org/mock) 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 injection. 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

internal/entity folder.
In MVC terms, entities are models.internal/usecase.The layer with which business logic directly interacts is usually called the infrastructure layer.
These can be repositories internal/repo/persistent, external webapi internal/repo/webapi, any pkg, and other
microservices.
In the template, the infrastructure packages are located inside internal/repo.
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.
Overall
Maintained
Code review
Go module
Latest release
Releases this year
(top 24 of 33)
309 followers · starred Mar 2023
608 followers · starred Jun 2022
380 followers · starred May 2022
139 followers · starred May 2022