The Consolidation of Code Over Time
Codebases don't stay sprawling forever. Given enough years, they converge or they die. What one product's repos taught me about monorepos, deleting more than you add, and why the best month of a codebase can be the month it gets smaller.
7 min read
This month, the main ZoomProp repository lost more code than it gained. By a lot. Across October, its commits added roughly 371,000 lines and removed roughly 951,000. About 3,500 files were deleted and about 1,600 were added. One whole top-level folder of the old front-end structure is gone.
Those numbers are blunt (line counts always are), but the direction is the story. A codebase that's been worked on steadily since mid-2025, with more than 2,700 commits, got dramatically smaller while the product got better. That's what consolidation looks like when it finally happens.
I've been thinking about why it took this long, and what years of a single product's code have taught me about how codebases converge.
A product is a family of repos, then a home#
ZoomProp didn't start as one repository. It started as a lot of them. Looking back through GitHub, the lineage is easy to trace:
- Late 2020: the first ZoomProp server and API repos.
- 2021: an "old" version, a "v2," a dashboard, a pipeline and API docs, each in its own repo.
- 2022 to 2024: a dedicated organization with separate repos for the API, the data pipelines, infrastructure as code, ORM models, the marketing site, the marketing CMS, an admin UI, test kits and more.
- Early 2025: a "new" ZoomProp repo, then another app repo that carried hundreds of commits over its year of life.
- Mid-2025 to now: one monorepo with a Next.js app (product, marketing and back-end-for-front-end routes), a Python FastAPI back end, and a small Node service, in one place.
None of those early splits were mistakes. Each one made sense when it was created. A separate repo is the cheapest way to start something without asking permission from everything else. The problem is that a product is one thing to its users. Eventually the code has to admit that too.
Why codebases sprawl#
Sprawl is rarely a decision. It builds up from reasonable choices:
- Starting fresh feels faster than understanding. "Let's spin up a new repo" is often a way to avoid reading the old one.
- Teams map to repos. When a contractor, an agency or a new hire owns a piece, they often get their own repo, and the boundary hardens.
- Rewrites rarely finish. A rewrite starts beside the original, and for a long time both are live.
- Patterns change faster than code. State management, component libraries and data fetching all moved under us. Each era left a layer behind.
You can see that last one inside a single repo, too. ZoomProp's front end carried a containers folder, a components folder, a utils folder, a hooks folder, a stores folder and a lib folder. Those weren't six different ideas. They were the same idea expressed in at least three eras of React.
What converging actually looks like#
Consolidation isn't one heroic migration. In my experience it happens in three phases, and they take much longer than anyone plans for.
Phase one: one roof. Bring the separate repos under one repository with one install, one CI and one deploy story. It's ugly. Nothing is cleaner yet, but everything is now visible in one search. That alone changes behavior: people start finding the three versions of the same helper.
Phase two: one way. Pick the current pattern for each concern and write it down. For ZoomProp that meant pushing logic toward a shared library layer and treating components as presentation. In October, the library folder was the one place that grew substantially while almost everything around it shrank. The old containers folder was removed entirely, at more than 100,000 lines.
Phase three: delete. Once the new way is proven, the old way has to go, all of it. Not deprecated, not commented out, not "kept for reference." Deleted, with git as the archive.
Most teams stall between phase two and phase three. They adopt the new way for new code and leave the old way running underneath "until we have time." They never have time. The codebase ends up with both, and every new engineer has to learn both.
Deleting more than you add#
The month a codebase gets smaller is usually its best month. Here's why I believe that.
- Every line is a liability before it's an asset. Code has to be read, built, tested, scanned and understood. Unused code still costs all of that and returns nothing.
- Duplicates drift. Two copies of a helper become two slightly different helpers, and then a bug that only shows up on one screen.
- Agents read everything. This is newer. When agents work in a codebase, dead code isn't just clutter. It's misleading context. An agent that finds three patterns will copy whichever one it saw first. A smaller, consistent codebase is cheaper to give a model and harder for it to misread.
The agent loops I run work better on a codebase with one clear way to do each thing. I wrote about how much context quality matters in Skills, Standards and Context. Consolidation is the same idea applied to the code itself.
Signs it's time to consolidate#
You don't need a metrics dashboard to know. These are the signals I watch for:
- A new engineer asks "which one should I use?" more than once in their first week.
- The same bug gets fixed in two places, or worse, in one place.
- Search results for a common helper return several near-identical files.
- Agents keep copying an old pattern into new code, because it's the one they found first.
- Changing a shared concept, such as a user, a subscription or a property, needs pull requests in more than one repo.
Any one of these is normal. Three or more together usually means the codebase is paying a tax on every change.
Tools that make deletion safe#
Deleting a third of a codebase is only reasonable if you can prove you aren't deleting anything alive. The tools I lean on:
- Dead-code analysis. A tool that walks the import graph from real entry points and reports unused files, exports and dependencies. The repo has a config for one. Run it before arguing about what's used.
- Type checking across the whole tree. If deleting a file breaks a type somewhere, you find out in seconds.
- End-to-end tests on the core flows. The flows a customer actually uses. Unit tests on deleted code don't count.
- Small, reviewable waves. Delete one folder or one pattern per change, so a regression points at one cause.
- Separate workspaces for parallel work. While the main tree was being reshaped, larger feature efforts lived in their own full working copies next to it, one per effort, so the cleanup and the feature work didn't trip over each other.
What years of one product teach you#
A few lessons I'd tell my earlier self:
Monorepos aren't about tooling. They're about honesty. One repo forces one answer to "how do we do this here," and it makes the cost of not answering visible.
Split by deploy, not by team. Separate repos make sense when the pieces deploy, scale or fail independently. A separate repo because a different person is building it is a decision you'll undo later.
Write the pattern down before you migrate to it. A one-page "this is how we fetch data now" document saves weeks of review comments. It's also exactly what you hand an agent.
Rewrites belong inside the codebase. The successful convergence wasn't a new repo that replaced the old one. It was the new pattern growing inside the existing repo until the old pattern could be removed.
Budget for deletion. If a roadmap only has additions on it, the codebase will only get bigger. I now plan cleanup as real work with a definition of done: the old pattern is gone, not just unused.
History is the archive. People keep dead code "in case we need it." That's what version control is for. Deleting with confidence is a skill teams can learn.
The leadership side#
Consolidation is as much a people problem as a code problem. Every folder that gets deleted was someone's work, and sometimes a lot of it. I try to be explicit that removing code isn't a judgment on the people who wrote it. The code did its job for the product it was written for. The product changed.
It also helps to celebrate deletions out loud. A pull request that removes 20,000 lines and keeps every test green is one of the most valuable things an engineer can ship. Most teams don't treat it that way.
The takeaway#
Codebases converge or they collapse. The ones that last are the ones where someone keeps choosing one roof, one way, and then the hard part: deleting what's left of the old way.
This month the repo I spend the most time in got much smaller and much clearer. For me, that counts as progress, even though it shows up as a negative number.
Related
- Leadership
Write Down Why, Not Just What
A decision log tells you what was chosen. A decision dossier tells you why, what was rejected and what it costs. Here's the format I use, and the bug it found that every other document had missed.
Product · Architecture · Leadership6 min read
- Engineering
Four Years of One Product's Data Pipelines
ZoomProp's property data stack went from a scheduler on a single server, to a pile of scripts, to software-defined assets with a shared data model. Looking back across four years of repos, here's what changed, what stayed the same and what I'd do first next time.
Data · Architecture · Codebases6 min read
- Leadership
The Tech Job Exodus Isn't New: When Opening the File Stopped Being Enough
Around 2012 and 2013, knowing how to operate the tools stopped being enough to hold a job on the web. You had to be able to build. The AI shift feels like that again, only faster. What I saw then, what's different now, and what I'd do about it.
Careers · AI · Front-end · Leadership6 min read