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.tsx becomes src/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.ts re-exports the public API; consumers import from billing, not billing/internal/parser.
  • Python: src/billing/__init__.py defines what from 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.md is 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.