| name | go-clean-architecture |
| description | Use when scaffolding or refactoring a Go service into a framework-agnostic clean (hexagonal) architecture: Domain, Usecase, Repository, Delivery layers, inward dependency rule, 'framework/database is a detail'. Apply when untangling a monolith or checking whether business logic is testable without HTTP or DB. |
| user-invocable | false |
| license | MIT |
| compatibility | Designed for Claude Code or similar AI coding agents. Requires Go 1.21+. Framework-agnostic: works with Gin, Echo, Fiber, Chi, or net/http; swap freely. |
| metadata | {"author":"muratmirgun","version":"0.1.0","openclaw":{"emoji":"๐๏ธ","homepage":"https://github.com/muratmirgun/gophers","requires":{"bins":["go"]},"install":[]}} |
| allowed-tools | Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*) |
Go Clean Architecture
A Go service organized into four concentric layers โ Domain, Usecase, Repository, Delivery โ where source code depends inward only. Done well, the HTTP framework and the database are interchangeable details; the business logic is testable without either.
This skill is framework-agnostic. Swap Gin for Fiber, Echo, Chi, or net/http by replacing the delivery layer โ zero changes elsewhere.
Core Rules
- Dependency Rule. Source depends inward: Delivery โ Usecase โ Domain. Repository implements interfaces declared in Domain. Domain depends on nothing.
- Framework is a detail. Gin/Fiber/Echo/Chi/net-http types live only in
internal/delivery/. Usecases see plain Go values.
- Database is a detail. SQL, sqlx, sqlc, pgx, GORM live only in
internal/repository/. Usecases see repository interfaces.
- Domain owns the interfaces; layers below provide implementations.
UserRepository is an interface in internal/domain; the Postgres struct is in internal/repository and unexported.
- DTOs at the edges. Delivery layer maps HTTP request bodies to domain inputs and domain entities to response bodies. Usecases never see
*gin.Context, http.Request, or DB rows.
cmd/<binary>/main.go is the only place that knows the whole system. Wiring (DI) is explicit, framework-free Go code.
When This Pays Off
| Symptom | What clean architecture buys you |
|---|
| HTTP handlers contain SQL | Move SQL into a repository; handlers shrink to 5 lines |
| Tests need a running DB | Mock the repository interface; usecase tests run in milliseconds |
| Swapping web frameworks is a rewrite | Replace internal/delivery/http; nothing else touched |
| Business rules duplicated across handlers | Single usecase function, called by HTTP, gRPC, and a CLI |
| ORM hooks fire in surprising places | Repository methods are explicit; no hidden behavior |
If the service is a 200-line cron job, this skill is overkill. If it will live 3+ years and grow features, it's the cheapest insurance you can buy.
Project Structure
myapp/
cmd/
api/main.go # entry point: config โ DI โ start server
worker/main.go # different entry, same Domain & Usecase
internal/
domain/ # entities, value objects, repository INTERFACES, domain errors
user.go
order.go
errors.go
usecase/ # business logic; depends only on domain
user_usecase.go
order_usecase.go
repository/ # implementations of domain interfaces (Postgres, in-memory, ...)
user_postgres.go
order_postgres.go
delivery/ # framework-specific adapters
http/ # Gin/Echo/Chi/net-http handlers and routes
user_handler.go
order_handler.go
grpc/ # gRPC server adapters (if applicable)
pkg/ # exported, importable from outside (if you publish a library)
migrations/ # SQL migrations
config/
go.mod
Read references/domain.md, references/usecase.md, references/repository.md, and references/delivery.md for the per-layer responsibilities.
The Four Layers
| Layer | Package | Can import | Must not import |
|---|
| Domain | internal/domain | stdlib only | usecase, repository, delivery, frameworks |
| Usecase | internal/usecase | domain | repository (concrete), delivery, frameworks |
| Repository | internal/repository | domain, DB driver | delivery, frameworks |
| Delivery | internal/delivery/... | domain, usecase (via interface), framework | repository (concrete) |
A golangci-lint config with depguard enforces these rules at CI time.
Layer Sketches
package domain
type User struct { ID, Email, Name string; CreatedAt time.Time }
type UserRepository interface {
Get(ctx context.Context, id string) (*User, error)
Create(ctx context.Context, u *User) error
}
type UserService interface {
Create(ctx context.Context, in CreateUserInput) (*User, error)
}
type userUsecase struct{ repo domain.UserRepository }
func NewUserUsecase(repo domain.UserRepository) domain.UserService {
return &userUsecase{repo: repo}
}
type postgresUserRepo struct{ db *sql.DB }
func NewUserRepository(db *sql.DB) domain.UserRepository { return &postgresUserRepo{db: db} }
type UserHandler struct{ svc domain.UserService }
func NewUserHandler(svc domain.UserService) *UserHandler { return &UserHandler{svc: svc} }
Read references/domain.md, references/usecase.md, references/repository.md, and references/delivery.md for full code examples per layer.
Wiring in main.go
db, _ := sql.Open("postgres", cfg.DBURL)
userRepo := repository.NewUserRepository(db)
userSvc := usecase.NewUserUsecase(userRepo)
userH := delivery.NewUserHandler(userSvc)
r := gin.New()
r.POST("/api/v1/users", userH.Create)
_ = r.Run(cfg.Addr)
This is the only file that imports every internal package. Adding a feature touches each layer plus one DI line here โ predictable.
Read references/anti-patterns.md for the failure modes โ leaking *gin.Context into usecases, importing repository from delivery, returning concrete types instead of interfaces.
Error Flow
Repository Usecase Delivery
sql.ErrNoRows โ domain.ErrNotFound โ 404
unique violation โ domain.ErrConflict โ 409
validation rule โ domain.ErrValidation โ 422
unknown โ wrapped error โ 500 (logged)
Map domain errors to HTTP status codes in the delivery layer โ never in the domain. The mapping changes per transport (HTTP 404 โ gRPC NotFound).
Anti-Patterns
| Anti-pattern | Why it hurts | Do this instead |
|---|
*gin.Context parameter in a usecase | Locks the system into Gin forever | Pass context.Context and plain inputs |
Repository returns *sql.Rows | Usecase has to know about database/sql | Return domain entities only |
Concrete *userUsecase exported | Direct instantiation bypasses constructor (and the dependency rule) | Return domain.UserService from New... |
| Delivery imports repository directly | Skips the usecase; logic moves to handlers | Inject domain.UserService, not *postgresUserRepo |
| Same struct for DTO and Domain entity | Adding HTTP-only fields pollutes the domain | Separate request/response structs in delivery |
Domain importing errors.Is(err, gorm.ErrRecordNotFound) | Couples domain to GORM | Translate driver errors in repository to domain.ErrXxx |
| Wiring scattered across init() funcs | Implicit order, hard to debug | All DI in main.go, top-to-bottom |
Verification Checklist
Each item maps to a command you can run; the expected outcome is in parentheses.
References