npx skills add ...
npx skills add eduardo-sl/go-agent-skills --skill go-architecture-review
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". Not for: code-level style (go-coding-standards), endpoint design (go-api-design).
npx skills add eduardo-sl/go-agent-skills --skill go-architecture-review
Good architecture makes the next change easy. Bad architecture makes every change scary.
Pick the mode that matches the request before starting:
For repositories with many packages, build the dependency picture before judging it:
go list ./... for packages, then import statements
to trace dependency direction.file.go:line in every finding.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().main.go per binary, minimal logic inside.Dependencies MUST flow inward. Domain core has zero external dependencies:
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/.main.go is the composition root. Wire everything here:
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.
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.
Rules:
internal/config.Avoid init(). It runs implicitly, makes testing harder, and creates hidden dependencies.
Exception: registering drivers or codecs is acceptable in init():
cmd/ main packagesinit() with side effects (DB connections, HTTP calls)internal/ used for project-private packagesutils/, common/, helpers/ packageshandlers → services → domain ← stores
↓ ↓ ↓
(net/http) (pure Go) (database/sql)// ✅ Good — service defines the interface it needs
// internal/service/user.go
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 // depends on interface, not postgres.Store
}
// internal/store/postgres/user.go
type Store struct { db *sql.DB }
// Implements service.UserStore without importing the service package
func (s *Store) GetByID(ctx context.Context, id string) (*domain.User, error) { ... }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()
// Wire dependencies
userStore := postgres.NewUserStore(db)
userService := service.NewUserService(userStore)
userHandler := handler.NewUserHandler(userService, logger)
// Setup router
r := chi.NewRouter()
r.Mount("/api/v1/users", userHandler.Routes())
// Run server
srv := &http.Server{Addr: cfg.Addr, Handler: r}
// ... graceful shutdown
}// ✅ Good — clear purpose
package orderservice // business rules for orders
package postgres // PostgreSQL data access
package httphandler // HTTP transport layer
// ❌ Bad — grab-bag packages
package utils // what ISN'T a util?
package common // everything and nothing
package models // types without behavior// ❌ Bad — package name repeated in type
package user
type UserService struct{} // user.UserService
// ✅ Good
package user
type Service struct{} // user.Servicetype 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"`
}// ❌ Bad — hidden side effects
func init() {
db, _ = sql.Open("postgres", os.Getenv("DB_URL"))
}
// ✅ Good — explicit initialization
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
}func init() {
sql.Register("custom", &CustomDriver{})
}