---
title: "Folder Hierarchy"
slug: "folder-hierarchy"
category: "file-organization"
tags: ["file-organization", "folder-hierarchy", "structure", "feature-folders"]
status: "stable"
last_updated: 2026-05-14
summary: "Keep folders shallow, group by feature not by type, add index files at each level, and let one folder hold one concept."
related: ["[[file-organization/project-structure]]", "[[file-organization/naming-conventions]]", "[[file-organization/large-codebase]]", "[[coding/general-principles]]", "[[tooling/obsidian]]"]
---

## Overview

The folder tree is the second piece of documentation a reader sees, after the README. A shallow, predictable hierarchy lets agents and humans guess the right path on the first try. Deep, type-driven trees force a directory archaeology before any change. The rules below cap depth, group by feature, mandate index files, and enforce one concept per folder. They build on the file-naming rules in [[file-organization/naming-conventions]].

## Cap nesting at four levels

Past four directory segments below the project root, the path stops being readable.

- Good: `src/billing/invoice/parser.ts` (3 levels).
- Bad: `src/modules/services/billing/invoice/parsers/xml/v2/parser.ts` (8 levels).

Each segment must carry information the others do not. If two adjacent segments could be merged without ambiguity, merge them. `src/components/forms/login/LoginForm.tsx` becomes `src/forms/LoginForm.tsx`; the `components/` segment was filler.

The "four levels" rule is a heuristic, not a law. Cross it only when a real domain boundary justifies it.

## Group by feature, not by type

Type-grouped folders (`components/`, `services/`, `models/`, `utils/`) scatter the files for one feature across the tree. Feature-grouped folders keep related code together.

```
# Bad (type-grouped)
src/
├── components/
│   ├── UserCard.tsx
│   └── InvoiceTable.tsx
├── services/
│   ├── userService.ts
│   └── invoiceService.ts
└── models/
    ├── User.ts
    └── Invoice.ts

# Better (feature-grouped)
src/
├── user/
│   ├── UserCard.tsx
│   ├── service.ts
│   └── model.ts
└── invoice/
    ├── InvoiceTable.tsx
    ├── service.ts
    └── model.ts
```

When `user/` ships, every file ships. When `user/` deletes, every file deletes. Feature folders survive refactors; type folders fragment them.

The exception: a folder of genuinely cross-cutting utilities (`lib/`, `shared/`) is fine. It earns its place when at least three features import from it. Below that, duplicate.

## One folder equals one concept

A folder name is a noun phrase that describes what every file inside has in common. If a contributor opens the folder and asks "why is this file here?" the folder is misnamed.

- `auth/` holds authentication code. A login form, a JWT verifier, an OAuth callback. Yes.
- `auth/` also holds the analytics dashboard "because login events go to analytics." No. Move it to `analytics/` and have it import from `auth/`.
- `utils/` is the canonical bad folder. Almost every "util" belongs with its feature; what is left is usually one or two helpers that deserve a more specific home.

When a folder's purpose blurs, split it into two folders with sharper names.

## Add an `index` file at each level

Every directory that is a public surface has an index file that names what the directory exports or contains.

- TypeScript: `src/billing/index.ts` re-exports the public API. Consumers import from `billing/`, not from `billing/internal/parser`.
- Python: `src/billing/__init__.py` lists what `from billing import ...` exposes.
- Documentation: every content folder has `index.md` as the [[tooling/obsidian]]-style MOC (Map of Content).
- Quartz / Astro content: `index.md` is the folder's landing page.

The index file gives a reader a single entry point. It also makes the folder's contract grep-able: search for "from billing" to see what the rest of the codebase relies on. Internal files stay internal because they are not re-exported.

## Internal vs public split

When a folder grows beyond a single concept, mark which files are public.

- TypeScript: a `_internal/` or `internal/` subfolder; the `index.ts` does not re-export it.
- Go: `internal/` is enforced by the compiler. Anything under `internal/` cannot be imported from outside the module.
- Python: a leading underscore (`_helpers.py`) signals "do not import from here." Convention only; not enforced.

The internal/public split lets a folder refactor freely without breaking callers. Without it, every file is implicitly public and every change risks a downstream break.

## Avoid folders that hold one file

A folder with a single file is overhead with no payoff. Inline it.

- `src/parser/parser.ts` is worse than `src/parser.ts`.
- Wait until a second related file exists before adding the folder.
- Exception: framework conventions (Next.js `app/[slug]/page.tsx`, Quartz `content/<category>/index.md`) force the structure. Honor them.

The corollary: when a folder shrinks to one file via deletion, collapse it back.

## Mirror the runtime model in the folder tree

The folder layout should reflect how the system actually runs.

- A web app with pages: `pages/` or `app/` mirrors URL structure.
- A service with handlers: `handlers/` mirrors the routes it serves.
- A worker with queues: `workers/<queue-name>/` per queue.
- A batch job with steps: `steps/` mirrors the pipeline.

When the tree reflects the runtime, a stack trace points at a folder name and the reader knows where to look. When it does not, every bug starts with a search. See [[file-organization/large-codebase]] for scaling these patterns past a hundred packages.

## Related

- [[file-organization/project-structure]]
- [[file-organization/naming-conventions]]
- [[file-organization/large-codebase]]
- [[coding/general-principles]]
- [[tooling/obsidian]]
