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 the rewrite quote is 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 thisor 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:
- Anything in the risk column, worst first
- Whatever is blocking the thing you were hired to do
- Tests around the load-bearing parts before you change them
- 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 a rewrite is actually right
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
- Refactoring before mapping. You’ll improve the wrong thing and break something you didn’t know was load-bearing.
- Upgrading all the dependencies on day one. Do it after tests exist, one at a time.
- Trusting the README. It describes the system as it was when someone last updated it.
- Skipping the “get it running” step. If the only working environment is production, you have no safe place to learn.
- Judging by code quality. The question is risk, not taste.
Checklist
- It runs locally, and
SETUP.mdsays 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.