Go Clean template
Clean Architecture template for Golang services
Overview
The purpose of the template is to show:
- how to organize a project and prevent it from turning into spaghetti code
- where to store business logic so that it remains independent, clean, and extensible
- how not to lose control when a microservice grows
Go-clean-template is created & supported by Evrone.
This template implements four types of servers:
- AMQP RPC (based on RabbitMQ as transport
- MQ RPC (based on NATS as transport
- User Authentication — registration, login, JWT-based authorization
- Task Management — CRUD operations with status transitions (todo, in_progress, done)
- Translation — text translation with history tracking
Content
Domains
The template includes three fully implemented domains, each available across all four transports (REST, gRPC, AMQP RPC, NATS RPC).
User Authentication
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 |
- Passwords hashed with bcrypt
- JWT tokens with configurable expiry
- Auth middleware on all transports
Task Management
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 |
- Status transitions:
todo→in_progress→done(andin_progress→todo) - Pagination with
limit/offsetand optional status filter - Tasks scoped to the authenticated user
Translation
Text 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 |
Quick start
Local development
# Postgres, RabbitMQ, NATS
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
Full docker stack with reverse proxy
make compose-up-all
Check services:
- AMQP RPC:
amqp://guest:[email protected]:5672/
- Client Exchange: rpc_client
- Server Exchange: rpc_server
- NATS RPC:
nats://guest:[email protected]:4222/
- Server Exchange: rpc_server
- REST API:
- gRPC:
tcp://grpc.lvh.me:8081 | tcp://127.0.0.1:8081
- v1/auth.proto
- v1/task.proto
- v1/translation.history.proto
- PostgreSQL:
postgres://user:myAwEsOm3pa55@[email protected]:5432/db
- RabbitMQ:
guest / guest
- NATS monitoring:
guest / guest
- Jaeger (traces UI):
Observability
Distributed tracing is provided by OpenTelemetry. Spans are exported over OTLP/gRPC to a collector — Jaeger in the docker stack.
- Context propagation — W3C
traceparent+baggage, so a single trace spans all four transports. REST uses
pkg/rabbitmq/rmq_rpc/otel_carrier.go,
pkg/nats/nats_rpc/otel_carrier.go).
- Instrumented layers — use cases and repositories are wrapped in tracing decorators (
*/tracing.go), so a trace
- No-op when disabled — with
TRACING_ENABLED=falsea no-op tracer provider is installed, so instrumentation runs
- Sampling —
TRACING_SAMPLE_RATEis a parent-based ratio. Use1.0locally; lower it (0.1/0.01) in
- Internal routes (
/healthz,/metrics,/swagger) are excluded from tracing — only the versioned API groups are
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 |
Project structure
cmd/app/main.go
Configuration and logger initialization. Then the main function "continues" in
internal/app/app.go.
config
The 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.
docs
Swagger documentation. Auto-generated by swag library. You don't need to correct anything by yourself.
docs/proto
Protobuf 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-test
Integration tests. They are launched as a separate container, next to the application container.
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 4 servers:
- AMQP RPC (based on RabbitMQ as transport)
- NATS RPC (based on NATS as transport)
- gRPC (gRPC framework based on protobuf)
- REST API (Fiber framework)
- Handlers are grouped by area of application (by a common basis)
- For each group, its own router structure is created, the methods of which process paths
- The structure of the business logic is injected into the router structure, which will be called by the handlers
internal/controller/amqp_rpc
Simple 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/grpc
Simple 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_rpc
Simple 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/restapi
Simple 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/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.
- Methods are grouped by area of application (on a common basis)
- Each group has its own structure
- One file - one structure
internal/repo/persistent
A repository is an abstract storage (database) that business logic works with.
internal/repo/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:
- There is no routing inside RabbitMQ
- Exchange fanout is used, to which 1 exclusive queue is bound, this is the most productive config
- Reconnect on the loss of connection
pkg/tracing
OpenTelemetry 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.
Dependency Injection
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.
Clean Architecture
Key idea
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.
The main principle
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:
- Business logic (Go standard library).
- Tools (databases, servers, message brokers, any other packages and frameworks).
The inner layer with business logic should be clean. It should:
- Not have package imports from the outer layer.
- Use only the capabilities of the standard library.
- Make calls to the outer layer through the interface (!).
The outer layer has other limitations:
- All components of this layer are unaware of each other's existence. How to call another from one tool? Not directly,
- All calls to the inner layer are made through the interface (!).
- Data is transferred in a format that is convenient for business logic (
internal/entity).
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
Layers
Clean Architecture Terminology
- Entities are structures that business logic operates on.
internal/entity folder.
In MVC terms, entities are models.
- Use Cases is business logic located in
internal/usecase.
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:
- controller (in our case)
- delivery
- transport
- gateways
- entrypoints
- primary
- input
Additional layers
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.
Alternative approaches
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.