brainstorming
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or planning anything new, in software or out of it (a talk, a business, a renovation).
go-project-layoutScaffold new Go projects and services: directory structure, cmd/ and internal/ conventions, when to use a flat layout, module naming, and main package wiring. Use when: "new Go project", "scaffold a service", "create project structure", "start a Go module", "how do I organize a new service", "set up folder structure". Not for: reviewing an existing architec…
Independent GitHub snapshot · Automated classification · Original documentation governs
Structure follows size. The biggest layout mistake in Go is copying a microservice skeleton for a 500-line tool — or growing a 50-package service inside a flat directory. Match the layout to the project.
| Project | Layout |
|---|---|
| Small tool, single binary, <5 files | Flat: everything in package main at the root |
| Library for others to import | Root package named after the module, internal/ for helpers |
| Service with one binary | cmd/<name>/main.go + internal/ packages |
| Multiple binaries sharing code | cmd/<name1>/, cmd/<name2>/ + internal/ |
Never start with empty pkg/, api/, docs/, build/ directories
"for later". Add structure when the code demands it, not before.
# ✅ Good — repository path, lowercase
go mod init github.com/acme/payment-service
# ❌ Bad — not fetchable, uppercase, or vanity without DNS
go mod init PaymentService
go mod init payment_service
The last path element should match what users will see: for a library, it becomes the default import name.
payment-service/
├── cmd/
│ └── payment-api/
│ └── main.go # flag/env parsing, wiring, Run() — nothing else
├── internal/
│ ├── domain/ # core types, business rules; zero external deps
│ ├── service/ # use cases orchestrating domain + stores
│ ├── store/ # data access implementations (postgres/, redis/)
│ ├── handler/ # HTTP/gRPC adapters
│ └── config/ # config loading and validation
├── migrations/ # if the service owns a database
├── go.mod
├── Makefile
└── README.md
Rules:
internal/ by default — the compiler enforces that nobody outside the
module imports it. Promote to a public package only on demand.pkg/ only when external consumers exist AND the module also has
private code. When in doubt, don't create it.handler → service → domain ← store.
domain imports neither store nor handler.Keep main.go to wiring plus a delegating call, so the app is testable:
func main() {
if err := run(context.Background(), os.Args[1:], os.Getenv); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run(ctx context.Context, args []string, getenv func(string) string) error {
cfg, err := config.Load(getenv)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
db, err := store.Open(ctx, cfg.DatabaseURL)
if err != nil {
return fmt.Errorf("open db: %w", err)
}
defer db.Close()
svc := service.New(store.NewUserRepo(db))
srv := handler.NewServer(cfg.Addr, svc)
return srv.ListenAndServe(ctx)
}
os.Exit appears exactly once, in main.run takes its dependencies (args, getenv) so tests can call it.init() functions for wiring — explicit construction order only.retry/
├── retry.go # package retry — the API, in the root
├── retry_test.go
├── backoff.go # same package, split by topic
├── internal/
│ └── clock/ # implementation details users must not import
├── examples_test.go # Example* functions shown in godoc
└── go.mod
src/, no lib/.util, common, helpers — name
packages after what they provide (retry, clock, httpsign).store/postgres, not store/postgres_impl.payment.Service, not payment.PaymentService.cmd/ are user-facing: cmd/payment-api,
hyphenated is fine (directory only holds package main).go.mod, go.sum, README.md, LICENSE, Makefile,
.golangci.yml, Dockerfile (single-binary projects).src/ (un-idiomatic), vendor/ (unless the team
explicitly vendors), one-file packages like types/ or models/
that become dumping grounds.go mod init <repo-path>.main.go with the thin-main pattern above.Makefile targets: build, test, lint.go build ./... and go vet ./... pass on the skeleton.internal/main.go is thin: parse, wire, call run, exitos.Exit only in main; no wiring in init()domain has zero infrastructure importsutil/common/helpers/models grab-bag packagesgo build ./... passes on the fresh skeleton