Forge
Codebase structure

Why codebase structure matters, and how to check yours

Structure decides what every change costs. In a well-organised codebase you can find where something lives, change it in one place and test it on its own. In a tangled one, every fix touches five files and breaks a sixth. AI coding agents struggle with tangled code too, for the same reasons people do.

Last reviewed by the Forge team

What goes wrong

What it costs when this is missed

High

Every change gets slower

When modules are tangled, a small feature touches many files, and estimates stop meaning anything.

High

Fixes that land in one copy

Duplicated logic drifts apart. A bug fixed in one copy stays alive in the others.

Medium

AI agents make it worse

Agents only read part of the code. When nothing has a clear place, they add new code instead of changing the right code, and the mess grows.

Medium

Nobody else can work on it

If only the author knows where things live, every new developer or contractor is slow, and every change is risky.

Common problems

What to look for in your code

Each one: why it hurts, and how to spot it in your own project.

01Unclear modules and boundaries

Each top-level area should have one job you can name in a few words. When responsibilities are spread around, nobody knows where a change belongs.

How to spot it: Look at your top-level folders. Can you say what each one owns? Does auth, billing or email live in one place, or in five?

02UI, business rules and data access mixed together

When components query the database and route handlers hold business rules, you can't test or reuse the rules, and changing the database means editing the UI.

How to spot it: Look for database queries in components or pages, and for business rules (prices, permissions, limits) inside route handlers.

Risky
// app/orders/page.tsx
const rows = await db.query("select * from orders where user_id = $1", [userId]);
const total = rows.reduce((s, r) => s + r.price * 1.2, 0); // VAT, again
Better
// app/orders/page.tsx
const { orders, total } = await listOrders(userId);
// lib/orders.ts owns the rules; lib/db owns the queries

03Circular imports and hub modules

When A imports B and B imports A, neither can change on its own. A 'utils' file everything imports turns every edit into a risk for the whole app.

How to spot it: Use a dependency graph tool (madge, dependency-cruiser) or your bundler's warnings to find cycles, then look at the files with the most importers.

04Duplicated logic

The same rule, type or query in several places means every change has to be made several times, and one copy is always forgotten.

How to spot it: Search for repeated function names, constants, regular expressions, SQL and fetch calls across folders.

05Oversized files and functions

A file that does five jobs is hard to read, review and test, and AI tools edit it less reliably because they see only part of it.

How to spot it: List your largest files and longest functions in the areas you change most. Split them along clear seams, one job each, not just by line count.

06No shared conventions

If errors, config and data fetching are handled three different ways, every reader has to relearn the codebase in each folder.

How to spot it: Pick three files from different areas and compare how they handle errors, fetch data and read config.

07Dead code

Unused files, exports nobody imports, commented-out blocks and stale feature flags look like real code. People (and agents) keep them working for nothing.

How to spot it: Use a tool such as knip or ts-prune to find unused exports and files, then delete them. Git keeps the history.

08Messy dependencies

Unused packages, two libraries doing the same job, and packages declared in the wrong place make builds slower and upgrades riskier.

How to spot it: Check that each package in your manifest is imported, and that you don't have two date, HTTP or validation libraries.

09Tests that don't cover the centre

The modules everything depends on are the ones whose breakage hurts most. If they have no tests, every refactor is a gamble.

How to spot it: Find the modules with the most importers and check that each has tests in a predictable place.

Quick self-check

How does your project do?

Tick each one that's true today. Anything left unticked is worth a closer look.

Tick what's true for your project. 9 checks.

How Forge checks this

A second opinion on your code, from the Structure scan.

  • Builds a code map from your imports and exports (cycles, hub modules, unused exports) before the AI reads anything.
  • Rates ten topics: modules, layering, coupling, duplication, size, conventions, dead code, dependencies, tests and tooling.
  • Tells you what to restructure first, and why.
  • Turns a finding into a step-by-step plan you can hand to your coding agent.

Questions

Does structure matter if the app works?

Working describes today. Structure is the cost of every change after today. It matters most once a project lives longer than a few weeks, or more than one person (or agent) works on it.

Should I restructure everything at once?

No. Fix the boundary that hurts most, usually where you change code most often, and leave stable areas alone. Big rewrites stall.

Why do AI coding agents struggle with messy code?

They work from the files they've read. When the same idea lives in three places, or a file's job isn't clear from its name, an agent is more likely to edit the wrong copy or add a fourth.

What's a good file size?

There's no hard rule; a few hundred lines is a common comfort zone. Split along clear seams, one job per file, rather than by line count alone.

Related guides

All guides

Sources