Project & File-Structure Theory for Scalable Applications
Audience:
- Humans designing applications
- LLMs generating or modifying codebases
Goal:
Define principles and patterns for structuring application folders/files so they scale with:
- Codebase size
- Team size
- Feature complexity
- Operational complexity (deployments, environments, etc.)
1. Why File Structure Matters (The “Theory” Bit)
File trees encode architecture. The way files are grouped influences:
- Coupling: How much changes in one area force changes in others
- Cohesion: How related the things in the same folder/module are
- Cognitive load: How hard it is to find, understand, and modify code
- Team boundaries: Who owns what, and how teams interfere with each other (Conway’s Law)
- Change patterns: Whether common changes touch one module or scatter across the repo
We’re not in “hard science” territory, but we do have decades of software engineering theory:
- Modularization (Parnas): Group by reasons for change, not by random technical detail.
- Information Hiding: Internal details stay behind clear interfaces.
- High cohesion, low coupling: The classic design heuristic.
- Conway’s Law: System structure tends to mirror org structure; structure should either reflect or intentionally resist this.
From this perspective, a “good” file structure is one where:
- Most changes touch a small number of modules.
- It’s obvious where new code should live.
- You can delete or replace a module without surgery across the codebase.
- Team boundaries and domain boundaries are aligned with the folder/module layout.
2. Design Goals for a Scalable Structure
When organizing files, aim to optimize for:
-
Change Locality
- When a feature changes, most edits happen in one folder/module.
- Example: “User Profile” feature mostly lives in
features/profile/.
-
Domain-Centric Thinking
- Structure reflects business/domain concepts (Orders, Billing, Auth) rather than only technical layers (Controllers, Services, Repos).
-
Stable Boundaries & APIs
- Boundaries (modules, packages, services) expose small, stable interfaces.
- Internals can churn; boundaries stay predictable.
-
Explicit Architecture
- There’s a clear “rules of the game”: e.g.
apilayer can depend ondomain, butdomainmust not depend onapi.
- There’s a clear “rules of the game”: e.g.
-
Scalability Along Multiple Axes
- More features: Add new feature modules without rewriting existing ones.
- More devs: Multiple teams can work with minimal merge conflicts.
- More services: If needed, feature or bounded context can be split out into a separate service.
-
Discoverability
- Given a task, a newcomer (or an LLM) can reliably guess where a file should be.
3. Two Fundamental Strategies: By Layer vs By Feature
Most project structures are variations or hybrids of these two.
3.1 Package-by-Layer (Horizontal Slices)
Group files by technical role:
src/ controllers/ services/ repositories/ models/ utils/Pros:
- Simple to understand.
- Works fine for small apps.
- Reflects typical textbook architectures (MVC, layered).
Cons:
- Scaling problem: a single feature often touches many layers across the tree.
- When app grows,
services/andutils/become grab bags. - Hard to isolate and extract features into separate modules/services.
When to use:
- Very small projects or prototypes.
- Educational / tutorial codebases.
- When team and domain are both tiny and stable.
3.2 Package-by-Feature / Domain (Vertical Slices)
Group by domain concept / feature, not by layer:
src/ users/ api/ domain/ infra/ billing/ api/ domain/ infra/ shared/ auth/ logging/Pros:
- High change locality: changes to “Billing” mostly stay in
billing/. - Easier to scale teams: “Team Billing” owns
billing/, “Team Users” ownsusers/. - Closer to how the business thinks (DDD-friendly).
- Natural path to modular monolith / microservices.
Cons:
- Slightly more upfront thought required.
- Some cross-cutting concerns (logging, auth, etc.) need a clear shared home.
When to use (recommended default):
- Anything beyond a small toy project.
- Systems you expect to grow in features, teams, or lifespan.
4. Architecture Styles That Influence File Layout
File structures should reflect architecture, not the other way around. Some key patterns:
4.1 Layered Architecture (N-tier)
- Layers like
presentation -> application -> domain -> infrastructure. - Mapped to folders:
src/ presentation/ application/ domain/ infrastructure/Guiding rule: dependencies go downward only (e.g. presentation can depend on application, but not vice versa).
You can combine with feature slicing:
src/ users/ application/ domain/ infrastructure/ billing/ application/ domain/ infrastructure/4.2 Ports & Adapters / Hexagonal / Clean Architecture
Key ideas:
- Domain core is pure and framework-agnostic.
- External systems (databases, APIs, UI) connect through ports (interfaces) and adapters (implementations).
Possible layout:
src/ users/ domain/ # entities, value objects, domain services application/ # use cases, orchestration ports/ # interfaces for persistence, messaging, etc. adapters/ # db adapters, API adaptersThis shines when:
- You want testable business logic.
- You anticipate swapping infrastructure (databases, messaging, etc.).
4.3 Domain-Driven Design (DDD) & Bounded Contexts
- Bounded context: a self-contained area of the domain with its own model and language (e.g.
Billing,Catalog,Support). - Excellent basis for both file structure and team structure.
Common mapping:
src/ billing/ # bounded context domain/ application/ infrastructure/ catalog/ domain/ application/ infrastructure/ shared-kernel/ domain/Bounded contexts can later be split into separate services without massive rewrites.
4.4 Modular Monolith & Microservices
- Modular monolith: a single deployable unit with strong internal module boundaries.
- Microservices: each module becomes its own deployable service, each with its own internal structure.
Monolith repo organized modularly:
src/ contexts/ billing/ catalog/ users/ shared/Microservices from that monolith:
services/ billing-service/ src/ billing/ catalog-service/ src/ catalog/ users-service/ src/ users/shared-libs/ logging/ auth/Key principle:
Design your monolith as if it were a set of services with clear contracts, even if it’s a single binary/run-time.
5. Practical Templates
5.1 Backend Service (Feature-Oriented, Modular Monolith Friendly)
.├─ src/│ ├─ core/ # cross-cutting, framework-agnostic logic│ │ ├─ shared/ # shared types, utilities, cross-cutting domain helpers│ │ └─ common/ # common infrastructure helpers (e.g. error types)│ ││ ├─ features/│ │ ├─ users/│ │ │ ├─ domain/ # entities, value objects, domain services│ │ │ ├─ application/ # use cases, orchestrations│ │ │ ├─ api/ # controllers/handlers/routes│ │ │ └─ infra/ # db access, external API integration│ │ ├─ billing/│ │ │ ├─ domain/│ │ │ ├─ application/│ │ │ ├─ api/│ │ │ └─ infra/│ │ └─ ...│ ││ ├─ config/ # app configuration, DI wiring│ └─ main.* # app entrypoint│├─ tests/│ ├─ unit/│ ├─ integration/│ └─ e2e/│├─ docs/└─ scripts/5.2 Frontend SPA (Feature-First)
src/ app/ routes/ layout/ providers/ features/ auth/ components/ hooks/ api/ state/ profile/ components/ hooks/ api/ state/ shared/ components/ hooks/ lib/ styles/Principle: each feature folder contains everything that feature needs; shared/ contains reusable pieces used by multiple features.
6. Choosing a Structure: A Simple Decision Guide
When designing or evolving structure (for humans or LLMs), follow this:
-
Identify domain areas or features.
- Example: Auth, Users, Billing, Catalog.
-
Treat each as a module / bounded context.
- Give each its own top-level folder.
-
Inside each feature/context, apply a simple, consistent internal pattern.
domain / application / api / infrais a solid default.- Alternatively:
components / hooks / api / statefor frontend.
-
Define dependency rules.
- Within a backend feature:
domaindepends on nothing internal.applicationdepends ondomain.apidepends onapplicationanddomain.infradepends ondomainand external libs.
- Across features:
- Prefer depending on interfaces or contracts, not concrete internals.
- Within a backend feature:
-
Create a shared area intentionally.
shared/for truly shared stuff.- Avoid dumping everything there; require a good reason before something becomes “shared”.
-
Evolve, don’t freeze.
- Start simple, reorganize when:
- A folder grows too large.
- A feature becomes clearly distinct enough for a module/context.
- You see recurring change patterns that suggest moving things closer.
- Start simple, reorganize when:
7. Anti-Patterns to Avoid
When designing or generating file structures, avoid these:
-
“Junk Drawer” Folders
- Names like
misc/,others/,tmp/,old/in main src tree. utils/that ends up containing unrelated stuff.
- Names like
-
God Modules / God Folders
- One mega-folder like
services/with dozens of unrelated services. - Single module doing orchestration for many unrelated features.
- One mega-folder like
-
Circular Dependencies in Structure
- Feature A imports deep internals of Feature B, and vice versa.
- Any structural pattern that encourages dependency cycles.
-
Framework-Driven Structure Only
- Structure mirroring framework internals instead of domain.
- Example:
controllers/,models/,views/at root for a large, complex domain.
-
Hidden Architecture
- No clear rules about what can depend on what.
- Random imports across the tree.
8. Instructions for LLMs Using This Document
When you (LLM) are asked to create or modify a codebase:
-
Detect / Choose an Architecture Style
- Small toy / script → simple layout is fine.
- Growing app → prefer feature/domain-first.
- Complex business domain → bounded contexts with DDD-ish layout.
- Testable, long-lived core → ports & adapters / hexagonal style.
-
Propose a File Tree Before Code
- Always output a proposed file tree first (as text).
- Explain briefly why it’s structured that way (feature-first, layered, etc.).
-
Default to Feature/Domain-First
- Top-level under
src/should usually befeatures/orcontexts/with domain names (e.g.users,billing,catalog). - Inside each feature, use consistent subfolders:
- Backend:
domain/,application/,api/,infra/. - Frontend:
components/,hooks/,api/,state/, etc.
- Backend:
- Top-level under
-
Enforce Dependency Rules in Your Code
- Avoid imports that violate the intended direction (e.g.
domainimportinginfra). - If necessary, introduce interfaces/ports to keep dependencies clean.
- Avoid imports that violate the intended direction (e.g.
-
Keep Shared Code Minimal and Intentional
- Only put things in
shared/when they are used by multiple features. - If a helper is only used by
users/, keep it insideusers/.
- Only put things in
-
Maintain Change Locality
- When adding a feature or change, keep most of the new/changed files inside a single feature/context folder.
- If changes span multiple features, consider whether this hints at:
- A missing shared abstraction, or
- Poorly drawn boundaries.
-
Refactor Structures Gradually
- If the user’s current project is unstructured:
- Identify natural clusters of files (by domain or responsibility).
- Propose a reorganization plan (incremental steps).
- Maintain backward compatibility where necessary (e.g., keep public APIs stable while moving internals).
- If the user’s current project is unstructured:
-
Document the Structure
- Always add or update a
ARCHITECTURE.mdorREADME.mdexplaining:- Chosen structure.
- Dependency rules.
- Conventions for adding new modules/features.
- Always add or update a
9. Example: Minimal ARCHITECTURE.md Template
You can reuse this in projects:
# Architecture Overview
## High-Level Style
- Architecture: Feature-first, layered, modular monolith- Top-level organization: - `src/features/*` — domain features (users, billing, etc.) - `src/shared/*` — shared utilities and cross-cutting concerns - `src/config/*` — configuration and wiring - `src/main.*` — application entrypoint
## Inside a Feature
Each feature directory follows:
- `domain/` — domain model and domain services- `application/` — use cases, application services- `api/` — HTTP handlers, controllers, route definitions- `infra/` — database repositories, external API clients, messaging
Example:```textsrc/features/users/ domain/ application/ api/ infra/Dependency Rules
domaindepends only on language standard libs andshared/domainutilities.applicationcan depend ondomainand define ports/interfaces.apican depend onapplicationanddomain.infracan depend ondomain, external libraries, and implements the ports fromapplication.
Across features:
- Features may depend on
shared/. - Avoid direct deep imports into another feature’s internals; if needed, create explicit interfaces in
shared/or a common module.
Adding a New Feature
- Create
src/features/<feature-name>/. - Add
domain/,application/,api/,infra/subfolders as needed. - Expose any externally-used API via:
- HTTP routes in
api/, or - Public application service in
application/.
- HTTP routes in
- Update this document if the structure changes in a significant way.
---
End of document.