Clean architecture is a set of good ideas wrapped in a lot of ceremony. The good ideas: keep business logic independent of delivery mechanisms (HTTP, gRPC, CLI) and storage (Postgres, Redis, files). The ceremony: ports, adapters, use-case structs, repositories with interface layers for everything. In Go, you can take the good ideas and leave most of the ceremony.
net/http or database/sql; handlers are thin wrappers that translate HTTP into function calls; storage is behind an interface the domain defines. Everything else is optional.What clean architecture actually means
Robert Martin's original formulation has one non-negotiable rule: the dependency rule. Inner layers (domain) must not depend on outer layers (infrastructure). The domain doesn't know about Postgres. The domain doesn't know about HTTP. It knows about your business objects and the operations on them.
Everything else — the concentric circles diagram, the use-case classes, the strict interface-for-everything approach — is interpretation. Go's implicit interfaces and small interface idiom make most of that interpretation optional.
The three layers in Go
We collapse the original four layers to three for most Go backends:
- Domain. Structs, interfaces, and pure functions. No framework imports. No
*sql.DB. This is your business logic. - Application. Service structs that implement use cases. Depends only on domain interfaces. Orchestrates calls to storage and external services.
- Infrastructure. HTTP handlers, Postgres stores, email senders, Stripe clients. Depends on application interfaces.
Directory structure
// Organised by domain concept, not by layer.
internal/
invoice/
invoice.go // domain: Invoice struct, Storer interface
service.go // application: InvoiceService
store.go // infra: Postgres implementation
handler.go // infra: HTTP handlers
user/
user.go
service.go
store.go
handler.go
cmd/
api/
main.go // wire everything togetherGrouping by domain concept keeps all the code for a feature in one place. You don't need to jump between a repositories/ folder, a services/ folder, and a handlers/ folder to trace a single flow.
What to skip
- An interface for every struct. Define an interface when you have two implementations or need to mock for tests. Not before. One implementation → concrete type.
- Use-case structs with a single method.
CreateInvoiceUseCasewith one method is a function with extra steps. Just write the function. - DTO mappers between every layer.Map when the domain model and the wire format genuinely differ. Don't map for the sake of separation.
The rules that matter
Two rules cover 90% of what clean architecture gets right in Go:
- Domain packages import nothing from infrastructure. Run
grep -r 'net/http\|database/sql' internal/in CI. Fail the build if domain packages import infrastructure. - Handlers are thin. A handler parses a request, calls a service method, and writes the response. If a handler has business logic in it, move the logic to the service.
Apply those two rules consistently and the architecture takes care of itself. The dependency injection post covers how to wire the layers together in main.go.
End of article · Thanks for reading