Fix

Vibe Coding Maintenance: Why Every Change Costs Four Times The Last One

Vibe-coded codebases compound maintenance debt fast. Every change is fragile because nobody trusts the code enough to extend it safely. Here is the catalogue of debt patterns and the stabilisation work I would propose.

Published
Author
Anton de Villiers
Read
approximately 7 min
Contents
  1. The Short Answer
  2. The Catalogue of Maintenance Debt
  3. Why The Obvious Fix Fails
  4. How I Diagnose Maintenance Debt
  5. How I Would Approach The Stabilisation Sprint
  6. Honest Variables That Change The Cost Shape
  7. Why I Wrote This
  8. Frequently asked questions

Six months ago the app shipped. It worked. Customers signed up. New features got built. Each one took longer than the last. A simple fix takes a day, then two days, then a week. New developers slow to a halt because nobody can predict what a change will break. The fragility is increasing faster than the feature surface.

This is maintenance debt in a vibe-coded codebase. The LLM wrote each feature without thinking about the next one. Every commit that did not refactor accumulated debt. The debt compounds because nobody trusts the code enough to make the structural changes that would slow it.

This article is the catalogue of debt patterns I find, the diagnostic method, and the stabilisation engagement I would propose.

The Short Answer

Vibe-coded codebases compound maintenance debt fast because the LLM optimises for "ship the next feature" without optimising for "make the next feature easier." The fix is a stabilisation sprint that tackles the highest-leverage debt items first: the parts that are touched in every change, the duplicated logic that has drifted apart, the absent test coverage on critical paths, and the documentation that does not exist.

The goal is not zero debt. It is predictable change. A developer should be able to estimate a small change at "an hour" and have that estimate be right.

The Catalogue of Maintenance Debt

Duplicate logic that has drifted apart. The same business rule exists in three places: a route handler, a component, and a utility. They were copied at different times. They no longer agree. A bug fix in one place leaves the others wrong.

Implicit dependencies. Function A calls function B with no parameters. Function B reads from a global. Function A's behaviour depends on something invisible. Refactoring B is dangerous because A might break in ways nobody can see.

Magic strings and numbers. Permission levels are integers (0, 1, 2, 3) with no enum and no documentation. Status values are strings ("active", "Active", "ACTIVE") with no normalisation. Find-and-replace is dangerous because not every "active" means the same thing.

Stale comments that no longer match the code. A comment says "this returns null on failure." The code throws. A comment says "deprecated, use X instead." X was deleted six months ago. Comments are worse than no comments when they lie.

Configuration scattered everywhere. Some config is in environment variables. Some is hard-coded. Some is in a database settings table. Some is in a JSON file. Changing the timeout for the payment service requires editing three files and one database row.

Branches and abandoned attempts. Three different implementations of the same feature, all in the codebase. The LLM tried one, then another, then a third. Two are dead. Nobody knows which.

Missing tests on the critical path. The checkout flow has no tests. The auth flow has one test that is skipped. The pricing calculator has tests that pass even when wrong, because they were written by the LLM along with the code and validate that the code returns what the code returns.

Onboarding documentation that does not exist. A new developer joins. There is no readme that says where things go. No "how to run the project locally" guide that works on the first try. No diagram of the system. The first week is archaeology.

If three or more describe your codebase, this article is for you.

Why The Obvious Fix Fails

"We will write more documentation." Documentation that is not generated by the code drifts the moment the code changes. Static documentation about a moving codebase is worse than no documentation; it actively misleads.

"We will add a code review process." Code review on a vibe-coded codebase is rate-limited by reviewer availability. Without structural improvements, every PR is "I cannot tell if this breaks anything." Reviewers default to approve-and-pray.

"We will move to a monorepo." Or away from a monorepo. The repository structure is rarely the problem. The problem is the absence of clear boundaries inside whatever structure you have.

"We will rewrite it." Sometimes this is right. Most of the time it loses the implicit knowledge in the code (the bug fixes, the edge cases, the customer-specific quirks) and you spend ten weeks rebuilding back to feature parity.

How I Diagnose Maintenance Debt

Step 1: Read the recent commit history

What I do. Look at the last 100 commits. Note which files appear most often. Note how often "fix" or "hotfix" appears. The commit history tells you where the pain lives.

Step 2: Time a small change end-to-end

What I do. Take a simple change request (one that should be 30 minutes) and walk through doing it. Note every file touched, every place where the change had to be made, every test that broke and why. The walk-through reveals the friction.

Step 3: Map the duplicate logic

What I do. Use a duplication detector (jscpd, simian, or similar) plus manual reading. Find the top ten places where logic is duplicated. Note which copies have drifted apart.

Step 4: Identify the under-tested critical paths

What I do. List the critical user flows (signup, login, checkout, key features). Check the test coverage on each. If the checkout has 5 percent coverage, that is the highest-leverage place to add tests.

Step 5: Audit the configuration surface

What I do. Find every place configuration lives. Identify what could be reasonably changed without code edits. Identify what currently requires code edits. The gap is the configuration debt.

The diagnosis produces a written report ranking debt items by leverage (how often they cost time) and by cost (how much work to fix). The fix order follows leverage divided by cost.

How I Would Approach The Stabilisation Sprint

Phase 1: Deduplicate the worst duplications (week one)

Pick the top three or four places where logic is duplicated, especially where copies have drifted. Pick a canonical implementation, replace the duplicates with calls to it, validate that behaviour stays the same. The codebase shrinks. Future bug fixes only have to happen once.

Phase 2: Add tests for the critical paths (week one to two)

Pick the two or three most important user flows. Add the minimum integration tests to lock in their behaviour. Not unit-test perfection. Just enough that future changes break loudly when they break the flow.

Phase 3: Centralise configuration (week two)

Pick a single source of truth for configuration. Either environment variables (twelve-factor style) or a single config file. Move scattered config into it. Document what each setting does. Now changing the timeout for the payment service is a one-place edit.

Phase 4: Replace magic values with named constants (week two)

Pick the five most common magic strings or numbers. Replace them with enums, named constants, or typed unions. The IDE now warns when someone uses an invalid value. Find-and-replace becomes safe.

Phase 5: Remove dead code and abandoned attempts (week three)

Three implementations of the same feature, two of which are dead, become one. The codebase shrinks. Onboarding gets faster.

Phase 6: Write the readme that matches reality (week three)

A short readme: where things go, how to run the project locally, how to add a feature, how to deploy. Not a treatise. A page someone can read in ten minutes and start working.

Phase 7: Documented handover

You keep the stabilised code, the test suite, the config layer, and the readme. You also get a "what to stabilise next" plan that grades the remaining debt items and tells you which ones pay back next.

Honest Variables That Change The Cost Shape

Codebase size. Larger codebases have more debt, but the leverage from fixing the top ten items is similar.

Active feature pressure. If features are being shipped weekly, the stabilisation runs alongside in branches. If feature work can pause, stabilisation is faster.

Existing test coverage. If there are tests, the stabilisation locks them in and adds where critical paths are exposed. If there are no tests, we add the minimum during stabilisation and a full coverage push is separate.

Team size. A solo founder/developer codebase stabilises faster than a five-developer codebase because there is less coordination cost. Both shapes are normal engagements.

Genuine rewrite candidates. Rare but real. The diagnosis says so honestly.

Why I Wrote This

Most articles on "managing technical debt" treat all codebases the same. This article exists because vibe-coded codebases have a specific debt profile: heavy on duplicated logic, light on tests, scattered on configuration, full of abandoned attempts. The fix is targeted at that profile, not generic. If your codebase fits the description, the technical assessment is the route in.

Frequently asked questions

Frequently asked questions

Will I have to stop adding features during the stabilisation?

No. Most stabilisation work happens in branches that ship incrementally. Feature work continues.

How long does stabilisation take?

Typically two to four weeks for a small app. Larger codebases scope longer.

Do you add tests as part of this?

Yes, the minimum needed to lock in stability. Full test coverage is a separate engagement.

Will the codebase be stable forever?

No codebase is. The goal is to slow the rate of debt growth and make changes predictable, not to eliminate maintenance.

Have a project in mind?

I review every enquiry personally. Tell me what you want to build and I'll tell you on the call if it's a fit.

Get in touch