The problem with tangled code
You write a Go web server. It fetches a user from SQLite, formats a JSON response, and returns a 200 status. It works. Two weeks later, you need to swap SQLite for PostgreSQL. You spend an afternoon rewriting SQL queries inside HTTP handlers. A month later, you add Redis caching. Now database logic, cache logic, and business rules are braided together in a single package. Testing requires a running database. Refactoring feels like defusing a bomb.
This happens when the outside world leaks into the core logic. HTTP handlers know about table schemas. Database queries know about JSON marshaling. Business rules know about network timeouts. When any one of those external concerns changes, you rewrite the whole stack.
Clean Architecture solves this by enforcing a single rule: dependencies point inward. The center of your application contains pure business logic. The outer layers handle databases, HTTP, logging, and configuration. The center never imports the outside. The outside imports the center. In Go, this translates to package boundaries and interface contracts.
What Clean Architecture actually means in Go
Clean Architecture is not a framework. It is a dependency discipline. You split your code into four conceptual layers: domain, application, infrastructure, and interface. Each layer only talks to the layer directly inside it. The domain layer knows nothing about SQL, HTTP, or JSON. The application layer knows nothing about PostgreSQL or Redis. It only knows about the interfaces the domain layer defines.
Think of it like a restaurant kitchen. The chefs follow recipes and handle ingredients. They do not care how the food reaches the customer. The waiters take orders and deliver plates. They do not care how the food is cooked. The delivery drivers handle logistics. They do not care about recipes. If the restaurant switches from gas stoves to induction burners, the chefs adapt, but the waiters and drivers keep doing exactly what they were doing. The dependency flows inward: logistics serve waiters, waiters serve chefs, chefs serve recipes.
Go makes this pattern natural because of implicit interface satisfaction. You do not need to declare that a struct implements an interface. You just write the methods. The compiler verifies the contract at compile time. This keeps the domain layer completely free of infrastructure imports.
Business rules don't care about HTTP status codes.
The domain layer: where business rules live
The domain layer holds your entities and the interfaces that describe how they are stored or processed. Entities are plain structs. Interfaces define behavior. Go convention dictates that public names start with a capital letter and private names start lowercase. The domain package exports what the rest of the application needs and hides implementation details.
Here is the simplest domain setup for a user management system:
// domain/user.go
package domain
// User represents a registered account in the system.
type User struct {
ID string
Name string
}
// UserRepository defines how user data is persisted and retrieved.
type UserRepository interface {
GetByID(id string) (*User, error)
Save(u *User) error
}
The User struct holds state. The UserRepository interface defines behavior. Notice that the interface lives in the domain package, not alongside the SQL implementation. This is the "accept interfaces, return structs" mantra in action. The domain says what it needs. The infrastructure says how it delivers it.
If you place the interface next to the implementation, you create a hidden dependency. The application layer would need to import the infrastructure package just to type a variable. By keeping the interface in the domain, the application only imports the domain. The dependency graph stays clean.
The application layer: wiring the rules
The application layer contains use cases. A use case is a single business operation: fetch a user, validate a password, calculate a discount. It depends exclusively on domain interfaces. It never touches database/sql, net/http, or encoding/json.
Dependency injection in Go is explicit. You pass dependencies through constructors. There are no hidden service locators or reflection-based containers. The wiring is visible in plain Go code.
Here is a use case that fetches a user:
// application/user_service.go
package application
import "yourmodule/domain"
// UserService coordinates user-related operations.
type UserService struct {
repo domain.UserRepository
}
// NewUserService creates a service with the provided repository.
func NewUserService(repo domain.UserRepository) *UserService {
return &UserService{repo: repo}
}
// GetUser retrieves a user by their identifier.
func (s *UserService) GetUser(id string) (*domain.User, error) {
return s.repo.GetByID(id)
}
The receiver name s is short and matches the type. Go convention favors one or two letter receivers over this or self. The constructor NewUserService takes the interface, not a concrete type. The method GetUser delegates to the repository. If the repository fails, the error bubbles up. The service does not swallow it.
At compile time, the Go compiler checks that whatever you pass to NewUserService satisfies domain.UserRepository. If you pass a struct missing the GetByID method, you get cannot use PostgresUserRepo (type *PostgresUserRepo) as UserRepository value in argument. The compiler catches interface mismatches before the program runs. This is why explicit constructors are preferred over struct literals with exported fields: they enforce the contract at construction time.
The use case is the brain. The database is just a memory aid.
Infrastructure and interfaces: the outside world
The infrastructure layer implements domain interfaces. It contains database drivers, cache clients, message queue producers, and external API wrappers. The interface layer handles HTTP handlers, CLI commands, gRPC servers, and WebSocket connections. Both layers import the domain and application packages. Neither imports the other.
Here is a PostgreSQL repository that satisfies the domain interface:
// infrastructure/postgres_repo.go
package infrastructure
import (
"database/sql"
"yourmodule/domain"
)
// PostgresUserRepo implements domain.UserRepository using PostgreSQL.
type PostgresUserRepo struct {
db *sql.DB
}
// NewPostgresUserRepo creates a repository backed by a database connection.
func NewPostgresUserRepo(db *sql.DB) *PostgresUserRepo {
return &PostgresUserRepo{db: db}
}
// GetByID fetches a user from the users table.
func (r *PostgresUserRepo) GetByID(id string) (*domain.User, error) {
var u domain.User
// QueryRow returns the first row or an error if none exists.
err := r.db.QueryRowContext(context.Background(), "SELECT id, name FROM users WHERE id = $1", id).Scan(&u.ID, &u.Name)
if err != nil {
return nil, err
}
return &u, nil
}
The context.Background() call is a placeholder. In production, you pass the request context through every long-lived call site. Context is plumbing. Run it through every long-lived call site. The if err != nil block is verbose by design. The Go community accepts the boilerplate because it makes the unhappy path visible. You do not wrap it in a macro or hide it behind a defer-recover pattern. You handle it or return it.
The interface layer wires the HTTP handler to the application service:
// interface/http_handler.go
package httpinterface
import (
"encoding/json"
"net/http"
"yourmodule/application"
)
// NewUserHandler creates an HTTP handler for user operations.
func NewUserHandler(service *application.UserService) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "missing id parameter", http.StatusBadRequest)
return
}
user, err := service.GetUser(id)
if err != nil {
http.Error(w, "failed to fetch user", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
})
}
The handler knows nothing about SQL. It calls the service, handles errors, and writes JSON. If you later swap PostgreSQL for MongoDB, you change the infrastructure package. The handler and service stay untouched. Concrete implementations are cheap. Swapping them is what matters.
Where things break: common pitfalls
Clean Architecture in Go fails when developers treat it as a rigid template instead of a dependency rule. The most common mistake is interface pollution. You define an interface for every tiny operation, then create a separate package for each one. The project becomes a maze of empty directories. Interfaces should model business capabilities, not database queries. If an interface only has one method and only one implementation exists, you probably do not need it yet.
Another trap is over-engineering small projects. A CLI tool that reads a config file and prints a greeting does not need four layers. You pay an architecture tax in boilerplate, constructor wiring, and package navigation. If the tax exceeds the benefit, skip the pattern. Write the simplest thing that works.
Goroutine leaks happen when background tasks wait on channels that never close. Always provide a cancellation path. If a use case spawns a worker, pass a context.Context and check ctx.Done() before blocking. The worst goroutine bug is the one that never logs.
Compiler errors usually point to mismatched signatures. If you change a domain interface to return ([]User, error) but forget to update the infrastructure implementation, the compiler rejects the program with cannot use PostgresUserRepo (type *PostgresUserRepo) as UserRepository value in argument. The fix is straightforward: align the method signatures. Go does not allow partial interface satisfaction. You either implement the contract or you do not.
Interfaces are contracts. Don't sign one you can't enforce.
When to reach for this pattern
Use Clean Architecture when your business rules outlive your technology choices and you need to swap databases, message brokers, or transport protocols without rewriting core logic. Use a single monolithic package when you are building a quick script, internal tool, or prototype where speed matters more than long-term maintainability. Use a layered MVC pattern when your application is purely data-driven with minimal business logic and you want straightforward request-response mapping. Use explicit constructor wiring when you want full visibility into how components connect and you prefer compile-time guarantees over runtime reflection.
Architecture is a tax. Pay it only when the project grows.