Skip to content

Commit fcc5927

Browse files
committed
docs: add go-package-layout-guide (flat pkg/ default + split triggers)
1 parent 7f7ee68 commit fcc5927

5 files changed

Lines changed: 130 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ Please choose versions by [Semantic Versioning](http://semver.org/).
88
* MINOR version when you add functionality in a backwards-compatible manner, and
99
* PATCH version when you make backwards-compatible bug fixes.
1010

11+
## Unreleased
12+
13+
- docs: add `docs/go-package-layout-guide.md` — flat `pkg/` default, five subpackage-split triggers (independent reuse, versioning, cycle break, navigation friction, build-tag isolation), conventional always-split exceptions (`pkg/factory/`, `pkg/handler/`), GOOD/BAD code pair, premature-split → flat migration steps. Indexed in README.md (Go — Architecture & Patterns), llms.txt, and CLAUDE.md Doc↔Agent table (owner: `go-architecture-assistant`).
14+
1115
## v0.23.0
1216

1317
- feat: add RULE `go-composition/no-same-package-private-helper-for-business-logic` (MUST, judgment-tier, owner go-architecture-assistant) — flags new business logic added as a same-package private helper instead of behind an interface + constructor + struct + method seam. Closes the gap left by the cross-package version (which only matches `pkg.Func` syntax). Codified after PR bborbe/recurring-task-creator#16 shipped a private `buildFrontmatter` helper that local pr-review dismissed as "pre-existing pattern" (broken-windows fallacy).

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Each enforceable guide in `docs/` should have a matching agent in `agents/`. The
3232
| `go-state-machine-pattern.md` | `go-architecture-assistant` |
3333
| `go-service-implementation-patterns.md` | `go-architecture-assistant` |
3434
| `go-kubernetes-crd-controller-guide.md` | `go-architecture-assistant` |
35+
| `go-package-layout-guide.md` | `go-architecture-assistant` |
3536
| `go-functional-options-pattern.md` | `go-quality-assistant` |
3637
| `go-doc-best-practices.md` | `godoc-assistant` |
3738
| `go-testing-guide.md` | `go-test-quality-assistant` |

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ All guides live in [`docs/`](docs/) and can be read standalone without the plugi
9494
| [Composition](docs/go-composition.md) | Struct embedding |
9595
| [Concurrency](docs/go-concurrency-patterns.md) | Goroutines, channels |
9696
| [State Machine](docs/go-state-machine-pattern.md) | Phase-dispatched workflows, resumable multi-step processes |
97+
| [Package Layout](docs/go-package-layout-guide.md) | Flat `pkg/` default; subpackage split triggers |
9798

9899
### Go — Code Quality
99100

docs/go-package-layout-guide.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Go Package Layout Guide
2+
3+
Default: **one flat `pkg/` directory**. Split into subpackages only when the flat layout gets too big to navigate.
4+
5+
## Default Layout Rule
6+
7+
Put all production code into `pkg/<repo-name>` (or just `pkg/` with a single package name) as a flat package. Files are organized by file-name convention (`<type>.go`, `<type>_test.go`), not by directory.
8+
9+
**Two conventional exceptions** (these always split, even when they only have 1-2 files):
10+
11+
- **`pkg/factory/`** — every project gets one. Centralizes `Create*` wiring per [go-factory-pattern.md](go-factory-pattern.md). The factory's whole job is to be the one place that depends on every other package, so it MUST live in its own package to avoid making everything else depend on it.
12+
- **`pkg/handler/`** — every HTTP service gets one. Per [go-http-handler-refactoring-guide.md](go-http-handler-refactoring-guide.md), HTTP handlers always live in `pkg/handler/`, never inline in `main.go`. Even a single `/healthz` handler goes here.
13+
14+
Both exist by convention regardless of file count. The rest of the code is flat in `pkg/` until a real trigger fires.
15+
16+
```text
17+
notification-service/
18+
├── main.go
19+
└── pkg/
20+
├── inventory.go # was pkg/schedule/inventory.go
21+
├── tasks-for-date.go # was pkg/schedule/tasks_for_date.go
22+
├── publisher.go # was pkg/publisher/publisher.go
23+
├── render.go # was pkg/publisher/render.go
24+
├── tick.go # was pkg/tick/tick.go
25+
├── factory/ # always its own package (see Rule)
26+
│ └── factory.go
27+
└── handler/ # always its own package (see Rule)
28+
├── trigger.go
29+
└── healthz.go
30+
```
31+
32+
One Go package. One import path. Files grouped by name, not directory.
33+
34+
### GOOD: flat `pkg/` with conventional exceptions split
35+
36+
```go
37+
// [GOOD] pkg/publisher.go
38+
package notification
39+
40+
type Publisher struct { /* ... */ }
41+
42+
func (p *Publisher) Publish(ctx context.Context, msg Message) error { /* ... */ }
43+
```
44+
45+
```go
46+
// [GOOD] pkg/render.go — same package, free function call
47+
package notification
48+
49+
func Render(tmpl string, data any) (string, error) { /* ... */ }
50+
```
51+
52+
```go
53+
// [GOOD] pkg/factory/factory.go — conventional exception, separate package
54+
package factory
55+
56+
import notification "github.com/example/notification-service/pkg" // pkg declares `package notification`
57+
58+
func CreatePublisher(/* deps */) *notification.Publisher { /* ... */ }
59+
```
60+
61+
### BAD: premature type-bucket split
62+
63+
```go
64+
// [BAD] pkg/service/publisher.go
65+
package service
66+
67+
type Publisher struct { /* ... */ }
68+
```
69+
70+
```go
71+
// [BAD] pkg/repository/store.go — now publisher.go must import a sibling
72+
package repository
73+
74+
import "github.com/example/notification-service/pkg/service"
75+
// every feature touches pkg/service + pkg/repository + pkg/handler
76+
```
77+
78+
The BAD layout invents three packages before any of the five split triggers fire — cross-cutting churn replaces cohesion.
79+
80+
## When to split
81+
82+
Extract a subpackage **only** when the flat layout hits a real friction point — never preemptively. Triggers, any one of these:
83+
84+
1. **Independent reuse** — another binary in the same module (or a different repo) needs to import the chunk without dragging the rest.
85+
2. **Independent versioning** — the chunk has its own release cadence different from the parent.
86+
3. **Cycle break** — a circular import between two clusters of files in `pkg/` can only be broken by splitting.
87+
4. **Too many files to navigate** — ≥30 files in `pkg/`, where naming convention alone no longer keeps `cd pkg && ls` readable. Even then, split by *natural seam* (the cluster with the highest internal cohesion), not by *type bucket* (one dir per "model", "handler", "factory" is anti-pattern — see [go-architecture-patterns.md](go-architecture-patterns.md)).
88+
5. **Build-tag isolation** — a chunk uses `//go:build linux` or similar and contaminates the parent's portability.
89+
90+
If none of the above hold, **keep it flat**. A 5-file extracted package costs more in import-path noise + cross-file refactoring friction than it ever saves.
91+
92+
## Why
93+
94+
- Flat `pkg/` minimizes import-path churn during refactors. Moving a function between files is free; moving it between packages rewrites every import.
95+
- Flat layout makes "is this exported?" obvious — every lower-case identifier is package-internal; every upper-case is the public API of the module. Subpackages multiply the public-surface decision per package.
96+
- Premature subpackage split locks in an architectural shape before the code has revealed its real shape. Flat first, split when forced.
97+
- Renaming files is cheap. Renaming directories is cheap. Renaming a package is expensive (imports, godoc, tests, mocks, downstream callers).
98+
99+
## Antipatterns
100+
101+
- **One dir per "concern"**`pkg/handler/`, `pkg/service/`, `pkg/repository/`. These create cross-cutting churn (every feature touches three dirs) and obscure cohesion. See [go-architecture-patterns.md](go-architecture-patterns.md).
102+
- **One dir per "domain entity"**`pkg/user/`, `pkg/order/`. Often correct for large products; premature for a single-binary service.
103+
- **Splitting because "it feels cleaner"** — feelings are not a trigger. Run the five-rule check above.
104+
- **Splitting to satisfy a linter (`funlen`, `gocognit`)** — extract a function/method instead. Splitting a package is the wrong knob.
105+
- **Flattening `pkg/factory/` or `pkg/handler/` into `pkg/`** — these are the two conventional exceptions (see Rule). Even with one file, they stay in their own package.
106+
107+
## Migration: subpackages → flat
108+
109+
When a service has prematurely split into `pkg/foo`, `pkg/bar`, `pkg/baz` and none of the five triggers apply:
110+
111+
1. `git mv pkg/foo/*.go pkg/` (rename the package declaration in each file to match `pkg/`'s package name)
112+
2. Rewrite imports — `find . -type f -name '*.go' -exec sed -i '' 's|module/pkg/foo|module/pkg|g' {} +`
113+
3. Resolve symbol collisions (rename internal helpers if two subpackages each had `func helper()`)
114+
4. `make precommit`
115+
5. Delete the empty subpackage directories: `rmdir pkg/foo pkg/bar pkg/baz`
116+
117+
Do this in one PR per service. Don't split the migration across multiple PRs — partial state (some imports rewritten, some not) is a worse problem than the original premature split.
118+
119+
## Related
120+
121+
- [go-architecture-patterns.md](go-architecture-patterns.md) — Interface → Constructor → Struct → Method (works inside one flat package or many)
122+
- [go-factory-pattern.md](go-factory-pattern.md)`pkg/factory/` is the canonical home for `Create*` wiring (always a separate package, see Rule)
123+
- [go-http-handler-refactoring-guide.md](go-http-handler-refactoring-guide.md)`pkg/handler/` is the canonical home for HTTP handlers (always a separate package, see Rule)

llms.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- [Composition](docs/go-composition.md): Struct embedding and interface composition
1616
- [Concurrency](docs/go-concurrency-patterns.md): Goroutines, channels, sync primitives
1717
- [State Machine](docs/go-state-machine-pattern.md): Phase-dispatched state machines for long-running, resumable, multi-process workflows
18+
- [Package Layout](docs/go-package-layout-guide.md): Flat `pkg/` default; subpackage split triggers (independent reuse, versioning, cycle break, navigation friction, build-tag isolation)
1819
- [Design Patterns](docs/go-patterns.md): Common Go design patterns
1920

2021
## Go — Testing & Quality

0 commit comments

Comments
 (0)