Fractal tree file structure
This project follows a fractal tree approach to file organization, where the structure of any part mirrors the whole.
This self-similar organization allows confident navigation without needing to understand the entire codebase.
Core principles
-
Recursive structure: Every directory follows the same organizational patterns, creating predictable navigation at any depth.
Developers should not need to learn the entire codebase structure to contribute meaningfully to any section.
-
No circular dependencies: Imports must form a directed acyclic graph.
Circular import chains turn the fractal tree into a generic graph, breaking the tree's integrity and causing runtime issues.
-
Organic growth: Start with a single file; extract to subdirectories only when complexity demands it.
No boilerplate structure upfront.
Group resources by functional purpose, never by file "shape" (no project-wide
,
, or
folders).
-
Encapsulation: Resources in a subdirectory are internal to the parent file unless explicitly re-exported.
A
directory is "owned" by
.
Direct imports from nested levels are prohibited—each sub-tree exports resources that can only be imported on the next level up.
-
Contextual sharing: Common logic lives at the closest common ancestor ("fork" in the tree).
The
directory exists at the
level because multiple entrypoints need it.
Place shared logic as deep in the tree as possible while still serving all dependents.
-
Present-state focus: Structure reflects current reality, not anticipated future needs.
Refactor freely as usage patterns evolve.
This eliminates over-engineering and enables formal linting enforcement.
Practical rules
Naming
All files and folders use
kebab-case for cross-platform compatibility with case-sensitive filesystems.
Enforced via
.
No index files
Avoid
files that enable implicit folder imports.
They cause path ambiguity where
could resolve to both
and
, and they hurt ESM compatibility.
Files as mini-libraries
Each file acts as a self-contained "mini-library" with cohesive exports serving a common semantic purpose.
If a file contains only one export, name the file after that export.
Avoid default exports unless externally required.
Outgrown files become sub-trees
When a file grows unwieldy, extract logic into a sibling subdirectory bearing the original filename:
text
my-app.ts → my-app.ts (keeps public API)
→ my-app/
├── config.ts
├── lifecycle.ts
├── lifecycle/
│ ├── something.ts
│ └── something-else.ts
└── helpers.ts
Only
imports from the
directory, and only
imports from the
directory – each file owns its namespace.
If
becomes unused, delete it together with its internal folder safely.
Relative paths within workspaces
All imports within a workspace use relative paths.
Avoid mixing path alias systems (e.g.
) with relative imports, as this creates inconsistency.
(This project uses
aliases for the
root as a convention.)
folder convention
Shared resources between sub-trees go into
path/to/common-parent/shared/
.
Think of
folders as lightweight
.
Contents of parent-level
folders remain accessible, but sub-tree
folders are internal to that sub-tree.
Multiple entry points
Projects may have several entry points (pages, API handlers, scripts, tests).
Keep their names distinct from mini-libraries using suffixes:
,
.
Entry points access shared resources but remain outside core logic.
Colocate unit tests
Place unit tests beside the files they cover:
pairs with
.
Integration and end-to-end tests live in separate directories outside the source tree root.
Exceptions are permitted
Partial adoption works.
Gradually migrate from leaves toward the root.
Imperfect implementation still provides benefits by clarifying dependencies in sections of larger codebases.
Scoped directories with prefix
Directories prefixed with
group related utilities under a namespace, similar to npm scoped packages:
text
src/shared/
├── @foo/
| ├── a.ts
| └── b.ts
├── @bar/
| ├── m.ts
| └── n.ts
├── x.ts
└── y.ts
This prevents naming collisions and clearly signals "this is a utility namespace, not a feature."
Import rules
As a consequence of encapsulation, imports should only target "public" resources:
typescript
// ✓ Correct: import from the mini-library entry point
import { something } from "../../../shared/foo.ts";
import { other } from "../../../shared/@scope/bar.ts";
// ✗ Incorrect: import from internal files (owned by their parent)
import { internal } from "../../../shared/foo/helpers.ts";
import { deep } from "../../../shared/@scope/bar/internal.ts";
// ✗ Incorrect: import from a scope directly (like npm, scopes aren't packages)
import { wrong } from "../../../shared/@scope";
Organic growth example
A real project evolves step by step.
Starting with a single file:
Extract when necessary:
text
example.ts
example/
├── do-x.ts
└── do-x.test.ts
Add shared logic between extracted modules:
text
example.ts
example/
├── shared/
│ └── do-common-thing.ts
├── do-x.ts
└── do-y.ts
When a second entry point (
) needs something previously nested, promote it to the closest common ancestor:
text
shared/
└── bar.ts
example.ts
example/
├── shared/
│ └── do-common-thing.ts
├── do-x.ts
└── do-y.ts
example-2.ts
Each step reflects actual code relationships without predicting future needs.
Anti-patterns
- Do not group files by type/shape rather than function (, , )
- Do not use files enabling implicit folder resolution and path synonyms
- Avoid default exports unless required by third-party
- Do not over-engineer structures for hypothetical future needs
- Do not prematurely split files before maintenance issues emerge (they may not)
- Do not import from a sub-tree's internal files (bypassing encapsulation)