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.
Example findingStructureMediumConfirmed in codePrice rules implemented in three placesapp/checkout/page.tsx:42const total = items.reduce((s, i) => s + i.price * 1.2, 0);same VAT rule in lib/invoice.ts:18 and app/api/quote/route.ts:9, already drifting
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.