project-layout
Skill1 starsChanged 29 days agoMITView on GitHub
Use when creating a new Go backend service, adding a package or directory to one, deciding where an interface or shared helper goes, or deciding whether to split a service. Covers cmd/ and internal/, packages by domain instead of by layer, consumer-side interfaces and concrete return types, package naming without util or pkg/, and when to split a modular monolith into services.
3 other projects use a version of this, 1 of them edited.
Adopted by
Most recently taken up by
- rafapeixer/java-skillsedited, first seen Oct 20, 2026
- huguryildiz/ALETHEIAtheir own version, first seen Jul 6, 2026
- jwd83/skillstheir own version, first seen Jun 18, 2026
Other versions3 projects use a version of this.
Word for word · 0
None found.
Edited · 1
- rafapeixer/java-skills★ 111% same
Wrote their own version · 2
- jwd83/skills★ 1same name
- huguryildiz/ALETHEIA★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository26 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- new-skillSkill
- review-skillSkill
- background-jobs-outboxSkill
- ci-guardrailsSkill
- concurrencySkill
- container-runtime-tuningSkill
- context-and-deadlinesSkill
- db-migrationsSkill
- errorsSkill
- go-version-policySkill
- http-serverSkill
- json-v2Skill
- logging-slogSkill
- mcp-server-goSkill
- observability-otelSkill
- outbound-http-clientSkill
- overload-protectionSkill
- performance-profilingSkill
- postgres-pgx-poolSkill
- scale-to-zeroSkill
- secure-defaultsSkill
- sqlite-single-nodeSkill
- sql-queries-and-transactionsSkill
- testingSkill
- wasm-pluginsSkill
The file
We split this skill into the rules it gives an agent
6 rules. 0 are common, 1 is distinctive, and 5 are very specific to this project.
0
Common
Most of the 3 edited versions we read keep these.
1
Distinctive
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 16
Put each binary in cmd/<name>/main.go and keep it to wiring: read config, build the domain packages, connect them, run.
- Very specific: line 17
Put every other package under internal/, never in pkg/ or at the module root, and move code to a separate module only when another …
- Very specific: line 22
Keep one module and one deployable until a domain has a measured need for independent scaling, deploy cadence or failure isolation, then …
- Distinctive: line 115
When reviewing code under this skill, confirm:
- Very specific: line 130
A library module: If the repository exists so others can import it, the exported packages are the product: put them at the module root or …
- Very specific: line 131
An interface the provider must own: When a package defines a contract that many unrelated implementations satisfy (io.Reader, a plugin or …
“Common” is measured: kept by at least 6 in 10 of the 3 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Project layout
The decision
Every Go service starts with a directory tree, and the tree decides which code can import which. A layout copied from another language (controllers/, services/, repositories/, models/) or from the unofficial golang-standards/project-layout repo (pkg/, util/) looks organized on day one and turns every feature change into edits across four packages, import cycles that force a common package, and interfaces nobody can fake. This skill fixes where binaries, domain code, interfaces and shared code go in a backend service, and when one module with one binary should become several services.
The rule
- Put each binary in
cmd/<name>/main.goand keep it to wiring: read config, build the domain packages, connect them, run. - Put every other package under
internal/, never inpkg/or at the module root, and move code to a separate module only when another repository must import it. - Organize
internal/by domain (internal/orders,internal/billing), with each package owning its types, logic and storage, not by layer (models,services,repositories). - Declare an interface in the package that consumes it, unexported and listing only the methods that package calls.
- Return concrete types from constructors (
func New(...) *Service), not interfaces. - Name packages after what they provide, never
util,utils,common,helpers,shared,baseormodels. - Keep one module and one deployable until a domain has a measured need for independent scaling, deploy cadence or failure isolation, then split along an existing
internal/package boundary.
Why
Rule 1. The official module layout guide (go.dev/doc/modules/layout) places commands under cmd/ and server logic under internal/. A main package cannot be imported, so any rule written there cannot be tested from another package and has to be copied when a second binary (a worker, a migration command) needs it. A run(ctx, ...) error function called from main keeps the wiring itself testable.
Rule 2. The Go toolchain refuses to build an import of a/b/internal/c from code outside the tree rooted at a/b. That is what lets you rename, split and delete packages in a service without breaking anyone. Code at the module root or in pkg/ has no such protection: the first other team that imports it turns your refactor into a breaking change. The layout guide recommends keeping packages in internal "as much as possible" and splitting shareable code into its own module; it never mentions pkg/, which adds a path segment that says nothing. A separate module gets its own version and changelog, which is what a real consumer needs.
Rule 3. Layer packages couple every feature to every layer. Adding a field to an order touches models, repositories, services and handlers, and a review cannot see the feature in one place. Layers also produce import cycles, which Go rejects at compile time: services imports repository, then repository needs a type from services, and the fix is a models or common package that everything imports. A domain package keeps the things that change together in one place and hides its storage behind its own methods, so orders cannot read billing's tables. This rule is practitioner consensus (Ben Johnson's "Standard Package Layout", Peter Bourgon's "Go best practices"), not an official Go document; it follows from the Google style guide's advice to keep coupled code together and name packages by what they do.
Rule 4. The Google Go style guide says the consumer of an interface should define it, and to avoid creating interfaces until a real need exists. When billing exports a BillingService interface with every method, orders depends on methods it never calls, every fake implements all of them, and adding a method breaks every fake in the repository. A one-method charger interface in orders documents exactly what orders needs, makes the fake four lines long, and means orders does not import billing at all. Go interfaces are satisfied implicitly, so main passing *billing.Service to orders.New is the compile-time check that the two still fit.
Rule 5. "Accept interfaces, return concrete types" (Google style guide, with error as the accepted exception). A constructor returning an interface hides the type's other methods and its documentation, forces a type assertion when a caller needs one more method, and makes the provider the owner of an abstraction it cannot know the shape of.
Rule 6. A package name is part of every call site: billing.Charge says what happens, utils.Charge does not. Catch-all packages attract unrelated code because nothing about the name says no, everything ends up importing them, and they become the hub of the import graph. The Google style guide lists util, common and helper as names to avoid. When a helper serves one domain, put it in that domain; when it is a real shared capability, name the package after it (money, httpserver, postgres).
Rule 7. A function call becomes a network call, with latency, retries, timeouts, partial failure, and a distributed transaction where there used to be one database transaction. Each service also brings its own deploy pipeline, minimum replicas, dashboards and on-call surface, a fixed cost that dominates the bill for a small team. Domain packages already give you most of the modularity: if orders only talks to billing through a charger interface, replacing *billing.Service with an HTTP client later is a change in main and one new package, not a rewrite.
The cost: domain packages sometimes repeat a small type (an Order in orders, an order reference in billing) instead of sharing one, and main grows a few lines of wiring per package. That duplication is what keeps the packages independent; share a type only when both sides must change together.
A layout that follows these rules:
cmd/app/main.go # wiring only
internal/billing/ # billing types, logic, storage
internal/orders/ # orders types, logic, storage, its charger interface
go.modGood example
See examples/good/.
// charger is what orders needs from billing, declared here by the consumer.
type charger interface {
Charge(ctx context.Context, customerID string, cents int64) (string, error)
}
// New returns a concrete *Service, not an interface.
func New(c charger) *Service {
return &Service{charger: c, orders: make(map[string]Order)}
}func run(ctx context.Context, logger *slog.Logger) error {
bill := billing.New(10_000)
ord := orders.New(bill) // *billing.Service satisfies orders' charger at compile time.
o, err := ord.Place(ctx, "cus_1", 2_500)
if err != nil {
return err
}
logger.Info("order placed", "order_id", o.ID, "charge_id", o.ChargeID)
return nil
}internal/orders declares the one method it calls and never imports internal/billing; cmd/app is the only package that sees both. The orders test fakes charger with a single method, and billing.New returns *Service, so callers see every method and pick the ones they need.
Bad example
See examples/bad/.
// MISTAKE: the provider declares the interface with every method it has, so
// each consumer depends on all of them and each fake must implement all of them.
type BillingService interface {
Charge(ctx context.Context, customerID string, cents int64) (models.Charge, error)
Refund(ctx context.Context, chargeID string) error
ListCharges(ctx context.Context, customerID string) ([]models.Charge, error)
}
// MISTAKE: returns the interface; adding a method breaks every fake, and
// callers cannot see the concrete type's documentation or extra methods.
func NewBillingService(store *repository.Store) BillingService {
return &billingService{store: store}
} // MISTAKE: orders reads billing's storage directly because they share a
// layer package and a store; billing can no longer change how it keeps charges.
if _, ok := s.Store.Charges[c.ID]; !ok {
return models.Order{}, fmt.Errorf("place order for %s: charge %s missing", customerID, c.ID)
}The bad module has the layout that gets generated most often for a "clean" Go service: pkg/utils, internal/models, internal/repository and internal/services, with a provider-side interface, a constructor returning it, one store shared by every feature, and a discount rule written in main. It compiles and runs; the cost shows up on the second feature, when every change touches every layer.
Checklist
When reviewing code under this skill, confirm:
- Every binary is
cmd/<name>/main.go, andmaincontains wiring only, with no business rules. - All non-
mainpackages are underinternal/; there is nopkg/directory and no library package at the module root. - Packages under
internal/are named after domains or capabilities, not layers (models,services,repositories,handlers,controllers). - No package is named
util,utils,common,helpers,sharedorbase. - Each domain package owns its storage; no package reads or writes another domain's tables or in-memory state.
- Interfaces are declared in the consuming package, are unexported unless another package must name them, and list only the methods that package calls.
- Constructors return concrete types (or
erroras the second result), not interfaces. - Domain packages reach each other through consumer interfaces wired in
main, and nomodelsorcommonpackage exists only to break an import cycle. - A proposal to split out a service names the measured scaling, deploy-cadence or isolation need and the
internal/package it splits along.
When not to apply
- A single small command. A 200-line CLI or tool with one package can keep
main.goat the module root with nocmd/orinternal/; the layout guide sayscmd/is not necessary in a repository that consists only of commands. Addinternal/when a second package appears. - A library module. If the repository exists so others can import it, the exported packages are the product: put them at the module root or in named subdirectories, still without
pkg/, and keep only implementation details ininternal/. - An interface the provider must own. When a package defines a contract that many unrelated implementations satisfy (
io.Reader, a plugin or driver API,http.Handler), the provider declares it. That is a standard extension point, not a dependency you are faking. - Constructors that pick an implementation at run time. A factory that returns one of several types based on config (a storage backend chosen by flag) returns an interface by design; keep that interface small and owned by the package that defines the choice.
- Generated code. sqlc, protobuf and OpenAPI generators emit their own layout; keep generated code in a package named after what it is (
internal/orders/db,internal/api/gen) and do not hand-edit it to match these rules. - Separate teams or hard isolation requirements. When two teams already deploy on different schedules, or a component handles regulated data that must run in its own security boundary, split into services now and record the reason; the cost in rule 7 is then the price of a real requirement.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

