Skip to content

Multi-File Project Architecture ​

A vibe-coded project starts small. One file, one prompt, one feature. It works perfectly. You add a second feature. Still fine. By the fifth feature, something has changed: the agent that once wrote clean, well-structured code in ten seconds now produces a tangled mess, loses track of where things live, and sometimes overwrites functionality you built last week.

This is not the agent getting worse. This is the project outgrowing the agent's ability to hold the whole thing in its working memory at once. When your codebase fits in a single file, the agent sees everything in one glance. When it spans thirty files across eight directories, the agent needs help understanding what lives where, what depends on what, and where new code should go.

The skill you need is not "write bigger prompts." It is structural: organizing your project so the agent can navigate it without getting lost, regardless of how many files you have.

What you'll learn

  • File organization is not about aesthetics — it is about giving the agent a mental map it can follow without loading every file into memory
  • A monolithic file confuses the agent because it has to reason about unrelated concerns simultaneously; a well-structured project lets the agent focus on one concern at a time
  • The agent navigates by convention, not by reading every file — when file locations are predictable (routes in app/routes/, templates in app/templates/), the agent knows where to look without being told
  • An architecture description in CLAUDE.md is the map the agent uses to orient itself — without it, the agent treats your project as an undifferentiated pile of files

The problem: the monolith that worked at 200 lines, then broke at 2,000 ​

Every project starts with a single file. A Flask app in app.py. A React component in App.jsx. A script in main.py. At 200 lines, this is fine — the agent can hold the entire file in context, understand every function, and make changes without stepping on anything. At 500 lines, it starts to struggle. At 2,000 lines, it is lost.

The problem is not the model's context window being too small. Modern models can handle tens of thousands of tokens. The problem is that a monolithic file mixes unrelated concerns, and the agent has to reason about all of them simultaneously. When you ask it to change the CSV export logic in a 2,000-line file, it also has to parse the user authentication code, the database migration functions, the email notification system, and the dashboard queries — none of which are relevant to CSV export, but all of which are in the same file. The agent either wastes tokens reasoning about irrelevant code, or it skims and misses something important.

Here is what a monolithic file looks like to the agent:

python
# app.py — 2,000 lines
# Lines 1-200: imports, app setup, configuration
# Lines 201-500: user authentication (login, logout, register, password reset)
# Lines 501-800: dashboard routes (filters, sorting, data aggregation)
# Lines 801-1200: report generation (PDF, CSV, email delivery)
# Lines 1201-1600: admin panel (user management, system settings)
# Lines 1601-2000: utility functions, error handlers, and a 300-line helper nobody remembers writing

When you ask the agent to "add date range filtering to the dashboard," it has to find the dashboard routes in a sea of unrelated code. It might accidentally modify an auth function that looks similar. It might add an import that conflicts with something in the admin panel. It might, in the worst case, refactor a utility function that three other features depend on, breaking everything.

The fix is separation of concerns: splitting the monolith into files organized by domain, so the agent can work on one concern without wading through everything else.

Options & when to use each ​

ApproachWhat it is good forWhat it costs youWhen to pick it
Single fileQuick prototypes, scripts, tools that do one thingAgent confusion above ~500 lines; unrelated concerns tangled together; no clear place for new codeThe first hour of a project. Move to multi-file as soon as you have two distinct features
Domain-based structure (auth/, dashboard/, reports/)Projects where features are mostly independent — changing the dashboard rarely touches the auth systemMore directories; harder to share code between domains; risk of duplicationFull applications where each domain has its own routes, templates, and logic
Layer-based structure (routes/, models/, templates/, services/)Projects where multiple features share the same database layer, templates, or API patternsCross-layer features span many directories; adding a feature means touching files in four different placesAPIs, backend services, and apps where the data layer is the center of gravity
Hybrid (domain-based with shared layers)Large projects where most code is domain-specific but some patterns (error handling, database access) are sharedComplexity; the agent needs a clear explanation of what goes whereProjects with 50+ source files. Start with pure domain-based or layer-based first, hybridize only when needed

For vibe coding, domain-based structure is usually the right starting point. The agent naturally thinks in terms of features: "add a dashboard" is a request about the dashboard domain, and the agent should find all dashboard code in one place. Layer-based structure forces the agent to jump between directories for a single feature — routes in one place, models in another, templates in a third — which multiplies the chance of putting something in the wrong file.

Layer-based vs domain-based: what the agent sees ​

Layer-based — all routes together, all models together, all templates together:

app/
  routes/
    auth.py       # login, logout, register routes
    dashboard.py  # dashboard display routes
    reports.py    # report generation routes
  models/
    user.py       # User model
    order.py      # Order model
  templates/
    auth/
      login.html
      register.html
    dashboard/
      index.html
    reports/
      revenue.html

Domain-based — each domain contains its own routes, templates, and logic:

app/
  auth/
    routes.py     # login, logout, register routes
    templates/
      login.html
      register.html
  dashboard/
    routes.py     # dashboard display routes
    templates/
      index.html
  reports/
    routes.py     # report generation routes
    templates/
      revenue.html
  shared/
    models.py     # User, Order models (shared across domains)
    utils.py      # shared utilities

For the prompt "add a CSV export to the revenue report," the agent with a domain-based structure knows exactly where to look: app/reports/. With a layer-based structure, it has to find the reports routes in app/routes/reports.py and the report templates in app/templates/reports/ — two different directory trees for one feature.

Build it: from monolith to navigable structure ​

Step 1: recognize when it is time to split ​

You do not need to architect your project perfectly on day one. But you do need to know the signals that it is time to split. Here are the three clearest signals:

  1. The agent starts putting code in the wrong place. If you ask for a new feature and the code lands in an existing file that has nothing to do with that feature, the agent is lost. It does not know where new code should go.

  2. The agent accidentally modifies unrelated code. You ask for a dashboard change and the agent also tweaks an auth function. This means the unrelated concerns are in the same file, and the agent treated the whole file as fair game.

  3. Your CLAUDE.md keeps getting longer to describe where things are. When you need a paragraph to explain "the CSV export logic is in app.py around line 1400, but the data fetching is in utils/data.py, and the formatting is in templates/macros/reports.html," your structure is confusing the agent.

Any one of these is reason enough to split. All three at once means your project is fighting you.

Step 2: split by domain, not by file type ​

Here is the before and after of a real project that was confusing the agent.

Before: the monolith that confused the agent

my-app/
  app.py              # 2,400 lines: everything
  models.py           # 600 lines: all database models
  utils.py            # 800 lines: helper functions, mix of concerns
  templates/
    base.html
    dashboard.html
    reports.html
    admin.html
    login.html
  static/
    style.css
    app.js

The agent struggled with every prompt. "Add filtering to the dashboard" produced changes in app.py, models.py, and utils.py because dashboard logic was scattered across all three. "Add a report export" added code to app.py that looked so similar to admin panel code that the agent accidentally refactored the admin panel.

After: restructured by domain

my-app/
  app/
    __init__.py        # App factory, configuration, middleware
    shared/
      models.py        # Shared models (User)
      utils.py         # Shared utilities (error formatting, date helpers)
    auth/
      routes.py        # Login, logout, register
      templates/
        login.html
    dashboard/
      routes.py        # Dashboard display, filtering, sorting
      queries.py       # Dashboard-specific database queries
      templates/
        index.html
    reports/
      routes.py        # Report generation, CSV export
      queries.py       # Report-specific database queries
      templates/
        revenue.html
    admin/
      routes.py        # User management, system settings
      templates/
        users.html
  templates/
    base.html          # Shared base template
  static/
    style.css
    app.js
  CLAUDE.md            # Describes this structure

After the restructure, the same prompts produced clean, localized changes. "Add filtering to the dashboard" touched only app/dashboard/. "Add a report export" touched only app/reports/. The agent stopped accidentally modifying unrelated code because the unrelated code was in a different directory.

Step 3: describe the structure so the agent respects it ​

Splitting files is not enough. You have to tell the agent about the structure, or it will treat the new directories as random and start putting code wherever feels convenient. This is what goes in CLAUDE.md:

markdown
## Architecture

The project uses domain-based organization. Each domain is a self-contained module with its own routes, queries, and templates.

- `app/__init__.py` — app factory, configuration, middleware registration
- `app/shared/` — models and utilities used by multiple domains (User model, error formatting, date helpers)
- `app/auth/` — user authentication (login, logout, registration, password reset)
- `app/dashboard/` — main dashboard with filtering and sorting
- `app/reports/` — report generation (revenue, user activity, CSV/PDF export)
- `app/admin/` — admin panel (user management, system configuration)

### Rules for adding code
- Every new feature gets its own domain directory under `app/`
- Routes go in `<domain>/routes.py`
- Database queries specific to a domain go in `<domain>/queries.py`
- Templates specific to a domain go in `<domain>/templates/`
- If a model or utility is used by more than one domain, it goes in `app/shared/`
- Never put domain-specific code in `app/shared/`
- Always add new domains to this architecture description

### When to split a domain
- If a domain's `routes.py` passes 500 lines, split it by sub-feature
- If three domains share the same pattern, extract it to `app/shared/`

This tells the agent exactly what goes where and, critically, what NOT to do: never put domain-specific code in shared. The agent now has a map.

Step 4: the split-or-keep decision ​

Not every file needs to be its own module. The two questions to ask:

Does this concern change independently? If changing the dashboard never touches the auth code, and changing the auth code never touches the dashboard, they are independent concerns and should be in separate domains. If changing one always requires changing the other, keep them together.

Can the agent find it predictably? The agent finds files by convention, not by reading every file. If the convention is "every domain has routes.py," the agent knows to look for app/<domain>/routes.py without being told. If the convention is "the dashboard logic is in app.py but sometimes in utils.py if it needs a database query and sometimes in helpers.py if it was written on a Friday," the agent has no predictable path to follow.

A practical rule: if you have to tell the agent where a specific piece of code lives more than once, it belongs in a predictable location. Move it there and update CLAUDE.md.

What goes wrong ​

MistakeHow you notice itThe fix
Splitting too earlyYou spend more time navigating between files than writing code. The agent creates tiny files with one function each because it thinks everything must be separateStay monolithic until you hit ~500 lines or one of the three signals in Step 1. Premature splitting is as harmful as no splitting
Splitting by technology instead of domainYou have routes/, models/, templates/, services/, and adding a single feature touches files in all five directoriesRestructure by domain. Every domain gets its own routes, templates, and logic. The agent works on one domain at a time, in one directory
Not updating the architecture descriptionThe CLAUDE.md still describes the old structure. The agent follows the stale map and puts code in directories that no longer exist or no longer make senseAfter every restructure, update the Architecture section of CLAUDE.md. The map must match the territory
Creating a utils.py or helpers.py that becomes a second monolithapp/shared/utils.py grows to 900 lines of unrelated functions. The agent treats it as a dumping ground because "shared" sounds like "put anything here"Give shared modules specific names: error_formatting.py, date_helpers.py, validation.py. Never name anything utils.py or helpers.py — they are dumping grounds by design
Deep nestingapp/dashboard/filters/date/pickers/custom.py — the agent cannot find anything because the path is a puzzleMaximum three levels of nesting inside a domain. app/dashboard/routes.py is fine. app/dashboard/components/filters/date/pickers.py is not
Making every file a domainYou have 15 domains for 20 files. Each "domain" has one route and one template. The structure is now more confusing than the monolith wasA domain should contain at least three files before it earns its directory. If a feature only has one route and one template, keep it in a parent domain or a misc/ catch-all until it grows

Confirm it worked ​

Give the agent a feature request that touches an existing domain: "Add a new filter to the dashboard that filters by user role." The agent should open app/dashboard/routes.py (or whatever your domain structure specifies), add the filter logic, and touch nothing outside the dashboard directory. If it opens files in other domains, your structure description is not clear enough.

Now give the agent a feature request for a new domain: "Add a notifications page that shows recent system alerts." The agent should create app/notifications/routes.py and app/notifications/templates/index.html, following the domain template from your CLAUDE.md. If it puts the notifications code in an existing domain (like shoehorning it into the dashboard), your architecture description is not telling the agent where new features should go.

The acid test: check CLAUDE.md. Does the architecture section describe where every domain lives, what files each domain contains, and the rules for adding new code? If you read it as a new developer joining the project, would you know where to put a new feature? If the answer to both is yes, your structure is working.

Next: Day 4 — Systems Design & Project Management