Building and Deploying a Go Clean Architecture Application: A Complete Guide
You can build and deploy a Go clean architecture application by compiling the binary from cmd/app/main.go, configuring MySQL credentials via Viper in config/config.yml, and running the resulting binary or containerizing it with a multi-stage Docker build.
The manakuro/golang-clean-architecture repository demonstrates how to structure a production-ready Go web service using clean architecture principles. This guide explains the layered architecture that separates domain logic from infrastructure concerns, then walks you through compiling and deploying the application.
Understanding the Clean Architecture Layers
The project organizes code into concentric layers following the Dependency Rule, where inner layers contain business logic and outer layers handle technical details like HTTP and databases.
Domain Layer
At the core, pkg/domain/model/user.go defines the User struct—a plain Go struct with GORM tags representing the business entity. This layer contains no imports from external frameworks or database drivers, ensuring business rules remain isolated from technical implementations.
Use-Case Layer
Located in pkg/usecase/usecase/user.go, this layer defines the UserUsecase interface exposing List() and Create() operations. The implementation orchestrates repositories and wraps database operations in transactions using the Transaction method from the underlying DBRepository interface.
Adapter Layer
Adapters translate between external systems and use cases. The HTTP controller in pkg/adapter/controller/user.go decodes JSON requests and invokes use-case methods, while pkg/adapter/repository/user.go implements the repository interface using GORM for MySQL persistence.
Infrastructure and Wiring
The pkg/infrastructure package contains router/router.go (Echo HTTP server setup) and datastore/db.go (MySQL connection logic). The pkg/registry/registry.go handles dependency injection, assembling controllers, use cases, and repositories so that cmd/app/main.go remains a thin entry point that merely boots the server.
Building the Application Locally
Follow these steps to compile and run the service on your development machine.
Prerequisites and Configuration
Clone the repository and create a YAML configuration file at config/config.yml:
database:
user: "dbuser"
password: "dbpass"
net: "tcp"
addr: "127.0.0.1:3306"
dbname: "mydb"
allowNativePasswords: true
params:
parseTime: "true"
server:
address: "8080"
The pkg/config/config.go loader uses Viper to parse this file into the global config.C struct at startup, constructing a MySQL DSN using mysql.Config.
Compilation and Execution
Build the binary and start the server:
go build -o app ./cmd/app
./app
The console outputs Server listen at http://localhost:8080 when successful. Test the API endpoints:
curl http://localhost:8080/users
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name":"Alice","age":"30"}'
Deploying with Docker
For production deployments, package the binary using a multi-stage Dockerfile to minimize image size and attack surface.
Multi-Stage Dockerfile
Create a Dockerfile in the project root:
FROM golang:1.19-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app ./cmd/app
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/app .
COPY config/config.yml ./config/config.yml
EXPOSE 8080
CMD ["./app"]
Build and run the container:
docker build -t go-clean-arch .
docker run -p 8080:8080 -v $(pwd)/config:/root/config go-clean-arch
Configuration Management
Viper automatically maps environment variables prefixed with ENV_ to configuration fields, allowing you to override config.yml values in containerized environments without rebuilding the image. Mount your configuration as a volume or inject secrets via environment variables.
Request Flow and Transaction Handling
Understanding how requests traverse the layers ensures you can extend the codebase without violating architectural constraints.
When a POST request hits /users, the CreateUser method in pkg/adapter/controller/user.go binds the JSON payload to a model.User struct and delegates to the use-case:
func (uc *userController) CreateUser(ctx Context) error {
var params model.User
if err := ctx.Bind(¶ms); err != nil {
return err
}
u, err := uc.userUsecase.Create(¶ms)
if err != nil {
return err
}
return ctx.JSON(http.StatusCreated, u)
}
The use-case in pkg/usecase/usecase/user.go wraps the creation logic in a database transaction:
data, err := uu.dBRepository.Transaction(func(i interface{}) (interface{}, error) {
u, err := uu.userRepository.Create(u)
return u, err
})
This ensures atomicity when persisting to MySQL via the GORM implementation in pkg/adapter/repository/user.go, while keeping transaction control at the business logic layer rather than in HTTP handlers.
Summary
- The domain layer remains pure with no external dependencies, as seen in
pkg/domain/model/user.go. - Use-case logic in
pkg/usecase/usecase/user.gohandles business rules and transaction coordination throughdBRepository.Transaction. - Adapters isolate HTTP handling (
pkg/adapter/controller/user.go) and database implementation details (pkg/adapter/repository/user.go). - Infrastructure packages provide concrete implementations for routing (
pkg/infrastructure/router/router.go) and data storage (pkg/infrastructure/datastore/db.go). - The registry pattern in
pkg/registry/registry.goenables clean dependency injection without framework magic. - Build the application with
go build -o app ./cmd/appand deploy using multi-stage Docker builds for minimal production images.
Frequently Asked Questions
What is the Dependency Rule in clean architecture?
The Dependency Rule states that source code dependencies must point only inward toward higher-level policies. In this repository, pkg/adapter/controller/user.go imports pkg/usecase/usecase, but the use-case layer knows nothing about HTTP or Echo, ensuring business logic remains framework-agnostic and testable without spinning up web servers.
How does transaction handling work across layers?
The use-case layer calls Transaction from the repository interface defined in pkg/usecase/repository, passing a function that executes within a database session. This allows the business logic to coordinate multiple repository operations atomically without leaking database connection details to controllers or exposing transaction semantics to the domain model.
Can I replace GORM with a different ORM or driver?
Yes. The repository interface in pkg/usecase/repository abstracts all persistence operations including FindAll, Create, and Transaction. You can implement a new adapter in pkg/adapter/repository using SQLx, raw database/sql, or a NoSQL driver without modifying the use-case or domain layers, provided you satisfy the interface contracts.
How should I manage secrets in production deployments?
Viper supports environment variable overrides automatically. Rather than baking credentials into config/config.yml inside the Docker image, mount the configuration file as a volume or inject environment variables prefixed with ENV_ to override sensitive values at runtime, keeping secrets out of the container layer and version control.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →