Architecture

Inheriting a codebase nobody documented

An open filing cabinet drawer packed with index cards
Photo by qwitka on Unsplash

The agency stopped replying. The contractor's last commit was fourteen months ago. The one developer who understood the billing module left in March, and the handover was a Google Doc with four bullet points and a password.

This is a more common starting position than anyone admits publicly, and the instinct it produces — rewrite it — is almost always wrong at this stage. You cannot rewrite something you cannot describe. The first two weeks should buy you a description.

Week one: get it running and change nothing

The single most valuable early artefact is a README that takes a new machine from clone to running application. Not because documentation is virtuous, but because writing it forces you to discover every undocumented dependency, environment variable and manual step, and those are where the landmines are.

  1. Clone it, and get it running locally. Write down every step, including the ugly ones.
  2. Find out how it is deployed, and whether that process is reproducible or lives in one person's shell history.
  3. Confirm backups exist and that a restore has been tested. An untested backup is a belief, not a backup.
  4. Rotate every credential you were handed. You do not know who else has them, and the honest answer is usually "several people, and at least one former contractor".
  5. Resist every urge to tidy. Reformatting the codebase in week one destroys the git blame history you are about to need.

Read the schema before you read the code

The database is the most honest documentation in any inherited system. Code lies about intent — it is full of abandoned abstractions and features that were half-removed. The schema records what the business actually did, because every row had to be written by something real.

Dump the schema and read it like a document. Which tables are large? Which have foreign keys and which are joined by convention in application code? Where are the nullable columns that suggest a migration nobody finished? A table called users_new is an entire chapter of organisational history.

Row counts tell you where the product is genuinely used. A feature with three rows after two years is a feature you can probably stop worrying about, whatever the code volume suggests. This is usually the fastest way to find out which half of the system matters.

A dense mass of tangled coloured wiring
It looks like this from the outside. Most of it turns out to be inert — the job of the audit is to find the strands that are actually carrying current.Photo by nathan_cima on Unsplash

Find the money path

Every product has two or three flows that generate the revenue: sign up, subscribe, check out, invoice, renew. Trace each one end to end, and write it down as a sequence — route, handler, queries, external calls, side effects.

This does two things at once. It gives you a map of the part of the system you cannot afford to break, and it surfaces the dependencies you did not know existed — the webhook nobody mentioned, the cron job on a machine not in the deploy pipeline, the report emailed by a script running under someone's personal account.

Everything outside the money path can wait. Prioritising by code quality rather than by business exposure is how audits burn a month on a beautifully refactored admin panel while the payment retry logic is still silently dropping failures.

Use git as the documentation nobody wrote

The repository history is an archaeological record, and most teams never read it. Three questions are worth asking of it:

  • Which files change most often? High-churn files are where the requirements were least understood. They are also where the bugs are, and where refactoring pays back fastest.
  • When did this line appear, and why? git log -S "someString" finds the commit that introduced a piece of code — the git log documentation covers the pickaxe options. The commit message is often the only explanation that exists.
  • Where did activity stop? A module with no commits in two years is either finished or abandoned, and the schema will tell you which.

Adam Tornhill's work on behavioural code analysis makes the case for treating version-control history as primary evidence about a codebase, and it holds up: churn plus complexity identifies the risky parts of a system more reliably than reading it front to back.

Write characterisation tests, not correct ones

There are no tests. You will be tempted to write tests that assert what the system should do. Do not start there. Write tests that assert what it currently does, including the behaviour that looks wrong.

This is Michael Feathers' characterisation test from Working Effectively with Legacy Code, and the reason it matters is that you do not yet know which oddities are bugs and which are load-bearing. That rounding quirk in the invoice total may be a defect, or it may be the thing that makes the numbers agree with an accounting system nobody told you about.

Once the current behaviour is pinned down, you can change things and find out immediately what you broke. Until then every change is a guess with a customer on the other end.

What not to refactor

Ugly code that works, is rarely touched and sits off the money path is not a problem. It is just ugly. The cost of changing it is real and the benefit is aesthetic.

When something genuinely does need replacing, replace it incrementally. Martin Fowler's strangler fig pattern — route traffic through a new implementation piece by piece while the old one keeps running — is the approach that survives contact with a system you do not fully understand, which describes every inherited codebase for at least a quarter. Monolith to microservices covers the same trade-off at the architectural level, and the same warning applies: the big-bang rewrite is the option that looks fastest and finishes last.

What a useful audit actually hands over

A report that says the code is "poor quality" is worthless. A useful one is specific enough to act on and honest about what it did not cover:

SectionWhat it answers
How it runsClone to running, and how it reaches production
The money pathThe flows you cannot break, traced end to end
Risk registerWhat could take the product down, ordered by likelihood × damage
Security findingsCredentials, dependencies, exposed endpoints, data handling
Cost of ownershipWhat it costs to run, and what is being paid for and unused
Prioritised planThe next five things, with reasons, in order
What we did not look atThe parts still unknown, stated plainly

That last row is the one that tells you whether to trust the rest of it. Nobody reads a system of any size exhaustively in two weeks, and a report implying otherwise is selling confidence rather than findings.

What we do

Inherited systems are half of what research, debug and analyze exists for. We take the handover, get it running, trace the flows that earn the money, and hand back the report above plus the fixes for anything urgent we find along the way.

Frequently the conclusion is that the system is in better shape than the client feared and needs three specific fixes rather than a rebuild. Occasionally it is worse, and the value is finding out before another year of budget goes into it. Either way the deliverable is the same: a description you can make decisions from. If the answer does turn out to be structural, scale and re-architecture is the work that follows, and why is my app slow covers the performance half.

Frequently asked questions

We inherited a codebase with no documentation — where do we start?

Get it running locally and write down every step, rotate all handed-over credentials, then read the database schema before the code. The schema is the most honest record of what the business actually does. Next, trace the two or three flows that generate revenue end to end. Do not tidy or reformat anything in the first fortnight — it destroys the git history you are about to need.

Should we rewrite a legacy codebase or maintain it?

Maintain it until you can describe it. A rewrite of a system nobody fully understands reproduces the bugs you did not know about and drops the behaviour customers silently depend on. When replacement is genuinely necessary, do it incrementally with the strangler fig pattern — route traffic to a new implementation piece by piece while the old one keeps serving.

How long does a codebase audit take?

Two to three weeks for a system of typical size, to produce something actionable: how it runs, the revenue-critical flows traced end to end, a risk register, security findings, running costs and a prioritised plan. Nobody reads a codebase exhaustively in that time, so a credible audit also states plainly which parts it did not cover.

What is a characterisation test?

A test that asserts what the code currently does rather than what it should do, including behaviour that looks wrong. In an inherited system you do not yet know which oddities are defects and which are load-bearing — that rounding quirk may be what keeps your totals agreeing with an accounting system nobody mentioned. Pin the current behaviour down first, then change things and see immediately what broke.

What should we do first if the original developers have left?

Rotate every credential in the handover before anything else. You do not know who still has them, and a shared password from a handover is the most common way an inherited system gets compromised — after the handover, not during it. Then confirm backups exist and that a restore has actually been tested, because an untested backup is a belief rather than a backup.

References

  1. StranglerFigApplicationMartin Fowler
  2. Working Effectively with Legacy CodeMichael Feathers / Pearson
  3. git-logGit Documentation
  4. CodeScene blog on behavioural code analysisCodeScene

Keep reading

A mechanical stopwatch stopped part-way through a count
Performance

Why is my app slow?

The rewrite is the most expensive possible response to a problem you have not measured. Here is the order to look in, and what each answer means.

An aerial view of a motorway backed up with traffic
Scale

Why MVPs Break Under Real Load

Success is the failure mode. The shortcuts that got you to launch are exactly the ones that break when launch works.

Let's put it into production.

Book a 30-minute call — you'll walk away with a scope, a timeline and a fixed price.

Book a call