Overview
The folder tree is the second documentation a reader sees after the README. A shallow tree grouped by feature lets agents and humans guess the right path on the first try. The rules below cover depth, grouping, folder purpose, public surfaces, and runtime mirroring. File naming is in naming-conventions.
Keep nesting to about four levels
Past about four directory segments under the project root, paths stop being readable. Each segment must carry information its neighbors do not; merge adjacent segments that could be one.
- Good:
src/billing/invoice/parser.ts. - Bad:
src/modules/services/billing/invoice/parsers/xml/v2/parser.ts. src/components/forms/login/LoginForm.tsxbecomessrc/forms/LoginForm.tsx;components/was filler.
Four is a heuristic. Cross it only for a real domain boundary.
Group by feature, not by type
Type folders (components/, services/, models/) scatter one feature across the tree.
# Type-grouped
src/components/UserCard.tsx
src/services/userService.ts
src/models/User.ts
# Feature-grouped
src/user/UserCard.tsx
src/user/service.ts
src/user/model.ts
When user/ ships or is deleted, every file moves with it. A shared lib/ is fine once at least three features import from it; below that, duplicate.
Give each folder one concept
A folder name is a noun phrase that every file inside fits. If a contributor asks why a file is there, the folder is misnamed. Keep auth/ for authentication; an analytics dashboard that reads login events belongs in analytics/ and imports from auth/. utils/ is the canonical bad folder: most of its contents belong with a feature, and the remainder deserves a specific name. Split a folder whose purpose blurs.
Expose a public surface at each package edge
Give every package a single entry point that names what it exports, and keep the rest internal.
- TypeScript:
src/billing/index.tsre-exports the public API; consumers import frombilling, notbilling/internal/parser. - Python:
src/billing/__init__.pydefines whatfrom billing import ...exposes. A leading underscore (_helpers.py) signals private by convention only. - Go: anything under
internal/cannot be imported from outside the module; the compiler enforces it. - Docs and content sites:
index.mdis the folder’s landing page and map (obsidian).
Keep barrel files at package edges, not inside a package. A barrel that re-exports a whole folder pulls every module into each importer’s graph, which slows bundlers, test runners, and IDEs. Internal and public separation lets a package refactor without breaking callers.
Inline folders that hold one file
src/parser/parser.ts is worse than src/parser.ts. Add the folder when a second related file appears, and collapse it when deletion leaves one. Framework conventions override this: Next.js app/[slug]/page.tsx and Quartz content/<category>/index.md require the structure.
Mirror the runtime model
Make the tree reflect how the system runs, so a stack trace points at a folder name: app/ or pages/ mirrors URLs, handlers/ mirrors routes, workers/<queue-name>/ has one folder per queue, steps/ follows a pipeline. Scaling these patterns past a hundred packages is in large-codebase.