| name | go-architecture-review |
| description | Review Go project architecture: package structure, dependency direction, layering, separation of concerns, domain modeling, and module boundaries. Use when reviewing architecture, designing package layout, evaluating dependency graphs, or refactoring monoliths into modules. Trigger examples: "review architecture", "package structure", "project layout", "dependency direction", "clean architecture Go", "module boundaries". Do NOT use for code-level style (use go-coding-standards) or API endpoint design (use go-api-design).
|
| license | MIT |
| metadata | {"version":"1.1.0"} |
Go Architecture Review
Good architecture makes the next change easy. Bad architecture makes every change scary.
Operating Modes
Pick the mode that matches the request before starting:
- Layout review (default) โ assess an existing codebase against the
sections below and report violations with severity.
- Refactor plan โ same assessment, but the deliverable is an ordered
migration plan (smallest safe steps first), not just findings.
- New service consultation โ asked "how should I structure X":
apply sections 1-3 as prescriptive guidance instead of review checks.
Auditing Large Codebases
For repositories with many packages, build the dependency picture before
judging it:
- Map the module:
go list ./... for packages, then import statements
to trace dependency direction.
- Run independent passes: (a) layout vs section 1, (b) dependency
direction vs section 2, (c) wiring and config vs sections 3+5,
(d) package design vs section 4.
- If your environment supports delegating work to parallel sub-agents
or tasks, assign each pass to one; synthesize at the end โ dependency
findings often explain layout findings.
- Cite package paths and
file.go:line in every finding.
1. Standard Project Layout
myproject/
โโโ cmd/ # Main applications (one dir per binary)
โ โโโ api-server/
โ โ โโโ main.go
โ โโโ worker/
โ โโโ main.go
โโโ internal/ # Private packages โ cannot be imported externally
โ โโโ domain/ # Core business types (entities, value objects)
โ โ โโโ user.go
โ โ โโโ order.go
โ โโโ service/ # Business logic (use cases)
โ โ โโโ user.go
โ โ โโโ order.go
โ โโโ store/ # Data access (repositories)
โ โ โโโ postgres/
โ โ โ โโโ user.go
โ โ โโโ redis/
โ โ โโโ cache.go
โ โโโ handler/ # HTTP/gRPC handlers (adapters)
โ โ โโโ user.go
โ โโโ config/ # Configuration loading
โ โโโ config.go
โโโ pkg/ # Public packages (use sparingly)
โ โโโ httputil/
โ โโโ response.go
โโโ migrations/ # Database migrations
โโโ api/ # API definitions (OpenAPI, proto files)
โโโ go.mod
โโโ go.sum
โโโ Makefile
Key Rules:
internal/ enforces encapsulation at the compiler level. Use it aggressively.
pkg/ is for genuinely reusable packages. When in doubt, use internal/.
cmd/ main packages should be thin โ wire dependencies and call Run().
- One
main.go per binary, minimal logic inside.
2. Dependency Direction
Dependencies MUST flow inward. Domain core has zero external dependencies:
handlers โ services โ domain โ stores
โ โ โ
(net/http) (pure Go) (database/sql)
Rules:
domain/ imports NOTHING from the project. No store, no handler, no config.
service/ depends on domain/ types and interfaces, NOT on concrete stores.
handler/ depends on service/ interfaces.
store/ implements interfaces defined in service/ or domain/.
- Circular dependencies are a ๐ด BLOCKER. The compiler catches them, but design should prevent them.
type UserStore interface {
GetByID(ctx context.Context, id string) (*domain.User, error)
Create(ctx context.Context, user *domain.User) error
}
type UserService struct {
store UserStore
}
type Store struct { db *sql.DB }
func (s *Store) GetByID(ctx context.Context, id string) (*domain.User, error) { ... }
3. Main Package Wiring
main.go is the composition root. Wire everything here:
func main() {
cfg := config.Load()
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
db, err := sql.Open("postgres", cfg.DatabaseURL)
if err != nil {
logger.Error("connect db", slog.Any("error", err))
os.Exit(1)
}
defer db.Close()
userStore := postgres.NewUserStore(db)
userService := service.NewUserService(userStore)
userHandler := handler.NewUserHandler(userService, logger)
r := chi.NewRouter()
r.Mount("/api/v1/users", userHandler.Routes())
srv := &http.Server{Addr: cfg.Addr, Handler: r}
}
Avoid dependency injection frameworks. Go's explicit wiring is a feature.
If wiring gets complex, use Google's wire for compile-time DI code generation.
4. Package Design Principles
One package = one purpose
package orderservice
package postgres
package httphandler
package utils
package common
package models
Avoid package stuttering
package user
type UserService struct{}
package user
type Service struct{}
Package cohesion over size
A package with 20 related files is better than 20 packages with 1 file each.
Split packages when they have distinct responsibilities, not when they get big.
5. Configuration
type Config struct {
Addr string `env:"ADDR" envDefault:":8080"`
DatabaseURL string `env:"DATABASE_URL,required"`
LogLevel string `env:"LOG_LEVEL" envDefault:"info"`
Timeout time.Duration `env:"TIMEOUT" envDefault:"30s"`
}
Rules:
- All config from environment variables (12-factor).
- Validate at startup, fail fast with clear messages.
- No config scattered across packages โ centralize in
internal/config.
- Never hardcode values. Not even "just for now."
6. Init Functions
Avoid init(). It runs implicitly, makes testing harder, and creates hidden dependencies.
func init() {
db, _ = sql.Open("postgres", os.Getenv("DB_URL"))
}
func NewStore(dsn string) (*Store, error) {
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, fmt.Errorf("open db: %w", err)
}
return &Store{db: db}, nil
}
Exception: registering drivers or codecs is acceptable in init():
func init() {
sql.Register("custom", &CustomDriver{})
}
Architecture Review Checklist
- ๐ด No circular dependencies between packages
- ๐ด Domain types have zero infrastructure dependencies
- ๐ด No business logic in
cmd/ main packages
- ๐ด No
init() with side effects (DB connections, HTTP calls)
- ๐ก
internal/ used for project-private packages
- ๐ก Interfaces defined at the consumer, not the producer
- ๐ก Configuration centralized and validated at startup
- ๐ก Dependency direction flows inward (handlers โ services โ domain)
- ๐ข Package names are short, singular, descriptive
- ๐ข No
utils/, common/, helpers/ packages
- ๐ข Main package is a thin composition root