How to take over an existing codebase nobody understands.

Inherited code nobody understands? A five-day method: map it before changing it, find the load-bearing parts, then decide what to touch and what to leave.

By UmaizPublished 9 min read

The first thing to do with an inherited codebase is nothing. Don’t fix, don’t refactor, don’t rewrite — map it. Until you can say what the system is, what it talks to and where the risk sits, every change is a guess, and the urge to start over is mostly a way of avoiding the reading. Give the mapping a week and the rewrite question usually answers itself.

This applies whether the previous developer was a contractor, a co-founder who left, or an AI session six months ago that nobody reviewed. The method is the same.

Why is the rewrite quote usually wrong?

When a developer looks at an unfamiliar codebase and recommends starting over, they’re often being honest about their own position rather than the code’s. Reading someone else’s system is slow, unglamorous work; rewriting is more fun and easier to estimate.

But the existing code, however ugly, encodes years of small corrections: the edge case someone hit in month three, the workaround for a provider’s undocumented behaviour, the validation added after a support ticket. A rewrite throws all of that away and rediscovers it one incident at a time. That’s the cost nobody puts in the quote.

Day 1: Get it running

You can’t map what you can’t run. Before reading a line of application code:

# What is this, and what does it depend on?
cat package.json          # or requirements.txt, pubspec.yaml, go.mod
cat .env.example 2>/dev/null || grep -rhoE "process\.env\.[A-Z_]+" src/ | sort -u
cat Dockerfile docker-compose.yml 2>/dev/null

Get a local instance up, even if it takes the whole day. Every environment variable you have to discover, every service you have to stand up, goes into a SETUP.md as you go. That file didn’t exist before; it’s the first deliverable.

If it won’t run locally, that’s a finding — and probably the most important one, because it means the previous developer’s machine was the only environment.

Day 2: Map the boundary

What does the system talk to? List every external dependency:

  • Databases, and which tables each service touches
  • Third-party APIs — payments, email, auth, storage, LLMs
  • Queues, cron jobs, scheduled tasks
  • Anything that calls in — webhooks, other services

For each one, note what happens if it goes away. A payment provider outage that takes down signup is a different risk from a broken analytics call.

Then trace three request paths end to end — signup, the main thing the product does, and whatever touches money. Write down each hop. This is where you find out how the system actually works, as opposed to how the folder structure suggests it works.

Day 3: Find the load-bearing parts

Every inherited codebase has a handful of places where the real risk lives. They’re usually:

  • Authentication and authorization — who can reach what
  • Payments — anything that charges or refunds
  • Data migrations — anything that changes the schema
  • Cron and background jobs — code that runs when nobody is watching
  • Anything with // don’t touch this or a suspiciously specific comment

Find them, read them properly, and mark them. These get changed last, with tests, and with a rollback plan. Everything else can be touched more freely.

# Where are the scary bits?
grep -rniE "TODO|FIXME|HACK|XXX|don.?t touch|temporary|workaround" src/ | wc -l
grep -rniE "cron|schedule|setInterval|queue" src/ -l

Day 4: Separate mess from risk

This is the step that changes what the rest of the work costs. Ugly code that works is a different problem from tidy code that loses data — and the temptation is to spend effort on the first because it’s visible.

| Mess (fix when convenient) | Risk (fix first) | |---|---| | Inconsistent naming and formatting | No backups, or none ever restored | | Long files, large functions | Records reachable by users who don’t own them | | Duplicated code that currently agrees | Migrations that can’t be rolled back | | Dependencies a few versions behind | Deployment only one person can perform | | No comments | Secrets in the repository |

Sort every finding into one column. Effort on the left buys very little. Effort on the right prevents incidents.

Day 5: Decide what to touch, in order

Now — and only now — write the sequence. Not a wish list. An order:

  1. Anything in the risk column, worst first
  2. Whatever is blocking the thing you were hired to do
  3. Tests around the load-bearing parts before you change them
  4. The mess, opportunistically, when you’re already in the file

For each item: what it unblocks, what it costs, and what can be left alone indefinitely. That last category is usually bigger than people expect, and saying so out loud is a relief for everyone.

When is a rewrite actually the right call?

Occasionally, yes. The honest signals: the framework is unmaintained and blocks a security fix; the data model is so wrong that every feature requires a workaround; or the mapping shows the system is genuinely small and the risk column is empty. In those cases, rewrite one piece at a time behind the existing interfaces — not the whole thing in one go.

Mistakes to avoid

  1. Refactoring before mapping. You’ll improve the wrong thing and break something you didn’t know was load-bearing.
  2. Upgrading all the dependencies on day one. Do it after tests exist, one at a time.
  3. Trusting the README. It describes the system as it was when someone last updated it.
  4. Skipping the “get it running” step. If the only working environment is production, you have no safe place to learn.
  5. Judging by code quality. The question is risk, not taste.

Checklist

  • It runs locally, and SETUP.md says how
  • Every external dependency listed, with what happens if it fails
  • Three request paths traced end to end
  • Load-bearing parts identified and marked
  • Every finding sorted into mess or risk
  • A sequence, not a backlog, with the “leave alone” list written down

If you’d rather have someone do the mapping and hand you the sequence, that’s an existing codebase rescue.

Questions people ask about this

How do you take over a codebase nobody understands?

Map it before changing it. Get it running, draw the boundary (what it talks to, where data lives), find the load-bearing parts, separate mess from actual risk, and only then decide what to touch — in order. Five days of that beats five weeks of guessing.

Should I rewrite an inherited codebase?

Rarely. A rewrite discards everything that works, reintroduces bugs already fixed, and always takes longer than quoted. It is right only when the core is genuinely unsalvageable — a dead framework, or a data model that can’t carry the product — and that is far less common than the quotes suggest.

What does a codebase takeover deliver?

A written map of the system, a list of the parts that are actually risky versus merely ugly, and a prioritised order of work. After that, every change is small and reversible, and nothing depends on the previous developer being available.

← All guides

Your AI-built app doesn’t need another prompt.

It might need an engineer.

If you’ve spent hours asking AI to fix the same problem and you’re going in circles, stop fighting the code. Send me what you’ve got — I’ll figure out what’s wrong and tell you what I’d do next.

No cleanup. No judgment. The messy version is usually the useful version.

Show me what’s stuck.

Don’t spend another six hours fighting the same bug. Send me the URL, repo, screenshot, error, or just tell me what you’re trying to make work. The messy version is fine.

Both open with a short template already filled in. I’ll take a look and tell you what I need.