Engineering

How to take over a codebase you did not write

Editorial · Reveneau · September 7, 2026

How to take over a codebase you did not write

Someone hands you a working system and leaves. Maybe an agency finished the contract, maybe the one engineer who understood it took another job, maybe you acquired the company. The system runs, customers use it, and you are now responsible for it.

The instinct is to read the code. That instinct is wrong, and it gets worse as the codebase gets larger.

Code tells you what the system does. It never tells you what it was supposed to do. And in a codebase you did not write, you cannot distinguish a deliberate rule from an accident by reading, because both compile and both run. The line that looks like a careless omission may be a workaround for a customer requirement from two years ago, and the line that looks like a thoughtful guard may be a leftover from a feature that was removed.

Here is the order that works. It is five steps and the first one involves no code at all.

1. Establish what the system must guarantee

Go and ask the people who run the business, not the people who wrote the software.

The question is not "how does it work". It is: which of these would be a serious problem?

  • A wrong number on an invoice.
  • One customer seeing another customer's data.
  • A record that disappears.
  • An email that goes out late.
  • A report that is quietly incomplete.
  • Someone doing something their role should not permit.

You will get a ranking within about twenty minutes, and it is a ranking nobody has ever written down. That list now guides your work. It tells you which parts of an unfamiliar system deserve your attention this week and which parts can wait indefinitely, and no amount of reading source code would have produced it.

Write it down. It becomes the first section of the document that should have been handed to you and was not.

2. Characterise before you change anything

Take the area at the top of that list, the one where a failure would be worst, and find out what it currently does.

Not what it should do. What it does. Feed it real inputs, including awkward ones, record the outputs, and assert them. If there is a bug in there, you capture the bug as expected behaviour, and that is correct at this stage, because the purpose is not to certify correctness. It is to build a way to detect changes you did not intend.

This is the single most valuable thing you can do in the first week, for two reasons. It gives you permission to make changes at all, since you will now find out if you break something. And the act of constructing the fixtures teaches you the structure of the system faster than reading it, because you have to understand the inputs well enough to produce them.

The technique in detail is in adding evals to an existing codebase. The short version is that characterising a module people are afraid to change often makes it safe to edit again without changing a single line of it.

3. Trace one request end to end

Pick one real request flow. A user signs in and loads their dashboard. An order is submitted. A webhook arrives from a payment provider.

Follow it the whole way: the entry point, the authentication, the permission check, every service it touches, the queries it runs, what it writes, what it emits, what the caller receives. Draw it on paper as you go.

An afternoon of this teaches you more about the architecture than a week of reading files, because you learn the system's actual structure rather than its directory structure. Those two things are frequently unrelated, and the gap between them is where most of the surprises are.

Do it twice, on two paths that feel different from each other. The second trace is where you start recognising patterns instead of collecting facts.

4. Go looking for the silent failures

The obvious problems are already known. Someone will tell you about them in your first week, usually with strong emotion.

The dangerous ones are the failures nobody can see. Ask directly: what could be wrong right now with nobody noticing? Then go and look at those specific places.

The usual candidates:

  • Money arithmetic, particularly rounding, currency conversion, and tax.
  • Permission checks that work for the simple case and were never tested for a user belonging to two organisations.
  • Anything that filters a list, where an over-broad filter shows too much and an over-narrow one hides rows the customer needed.
  • Background jobs, which can fail every night for months without a single complaint.
  • Deletion, and whether it reaches every copy including backups, logs, caches, and analytics.

This class matters more on a generated codebase. Veracode's Spring 2026 research across more than 150 models found generated code compiling and running correctly over 95 percent of the time while being secure in only about 55 percent of generations. Code that runs correctly and is not safe describes a silent failure exactly, and it is invisible in a demo and in a code review conducted at normal reading speed.

5. Change something small and release it

Your first change should be trivial. A copy fix, a log line, a one-character correction. Something where the code change is obviously safe.

Because the code is not what you are testing. You are testing the delivery process: can you build it, deploy it, watch it, and roll it back. Every inherited system has at least one undocumented step in that process, and you want to find it during a one-line fix rather than during an incident at eleven at night.

Note what you learn. The staging environment that has not been rebuilt in four months. The deploy that requires someone to run a command by hand. The credential that is stored only in one person's password manager. That list is what you actually inherited, and it is more urgent than anything in the source.

What is different about a generated codebase

Taking over hand-written legacy code follows a pattern most engineers know. The code shows its own history: the inconsistent module is where two people disagreed, the file with four styles in it is where the trouble is, the commented-out block records something that happened.

Generated code removes those cues. It tends to be larger, more uniform, and more plausible. The naming is consistent, the structure is tidy, and the same clean style is applied to the parts that were carefully specified and the parts that were guessed. Consistency stops being evidence of care.

Two practical adjustments.

Do not read tidiness as correctness. Judge a module by whether its behaviour is asserted anywhere, not by how it looks.

Check whether the existing checks can fail. Inherited suites frequently contain assertions that would pass against an empty implementation, and they pass, so nobody has questioned them. Break something deliberately and confirm the suite notices. If it does not, you have learned that the passing results mean nothing, which is better learned on day three than in month three. The catalogue of these failures is in when evals give false confidence.

The rewrite question

It will come up in the first fortnight, usually from you.

The answer is almost always not yet, and the reason is information rather than sentiment. A rewrite requires knowing which parts of the current behaviour are essential and which are accidental. That knowledge takes months to acquire, and a rewrite started before you have it discards undocumented behaviour that real customers depend on, which you then rediscover one support ticket at a time.

What usually removes the wish to rewrite is characterisation. Most of the impulse to rewrite comes from fear of touching something, and asserting its current behaviour removes the fear without the cost. Our fuller treatment of the decision is in when to rewrite vs refactor legacy code, and the technical debt view is in what is technical debt and when to pay it.

What to write down as you go

Keep one document. It does not need to be good, it needs to exist:

  • What the system must always guarantee, from step one.
  • The flows you traced, in whatever form you drew them.
  • What you deliberately do not understand yet, listed honestly.
  • The things that turned out to be known to only one person.
  • Every operational quirk you found while releasing the small change.

The unknowns section is the most valuable and the one people are most tempted to leave out. A confident diagram that quietly guesses at three components is worse than an honest list, because the next engineer will trust the diagram.

Two weeks of this on a moderate system and you can work safely. Skipping it and starting with a refactor generally costs more in incidents than the two weeks would have.

Thanks to the engineers who inherited systems from us and told us what was missing from the handover. Every gap they named is now a line in the list above, and the list of things only one person knew was the one nobody thought to ask for.

You learn a codebase by finding out what it promises, and then testing whether it keeps its promises.

Related guide: Handing over AI-written software.

Sources

Common questions

What is the first thing to do when inheriting an unfamiliar codebase?

Find out what the system must guarantee, by asking the people who run the business rather than by reading code. Code shows what the system does and never what it was supposed to do, and you cannot distinguish a deliberate rule from an accident by reading either, because both compile and run.

Why is reading the code first a mistake?

Because reading gives you behaviour without intent, and it gets worse as the codebase grows. A large codebase takes weeks to read and you retain a fraction of it, while the questions that actually protect you (what must never happen, what breaks silently, where the money moves) can be answered in a couple of conversations.

What does it mean to characterise existing behaviour?

Writing checks that record what the code currently does rather than what it should do, by feeding real inputs to the risky areas and asserting the outputs you observe. Bugs get captured along with correct behaviour, which is the point, since the goal is to detect unintended change rather than to certify correctness.

How do you find the risky parts of a system you do not know?

Ask which failures would be serious for the business, then trace those flows specifically. Money movement, permissions and data isolation, deletion, and any number a customer makes a decision from. Commit history helps too, since files changed frequently and recently are usually where both the complexity and the defects concentrate.

Why is a small change the right first task?

Because it tests the delivery process rather than the code: whether you can build, deploy, observe, and roll back. Discovering that the deploy process has an undocumented manual step is much cheaper during a one-line fix than during an urgent production issue.

What is different about inheriting an AI-generated codebase?

It is typically larger, more uniform, and more plausible-looking than a hand-written one, which removes the usual visual cues. Messy hand-written code shows where the trouble is through inconsistency, while generated code applies the same tidy style to the correct parts and the wrong parts alike, so consistency stops being evidence of care.

How long should takeover take before making real changes?

One to two weeks for a moderate system: a few days establishing guarantees and tracing flows, a few days characterising the risky areas, and one small released change. Teams that skip this and start with a refactor generally lose more time to the resulting incidents than the takeover period would have cost.

Should you rewrite a codebase you have just inherited?

Almost never at the start, because the decision requires knowing which parts are essential and which are accidental, and that knowledge takes months to acquire. The early rewrite also discards undocumented behaviour that real users depend on, which is usually rediscovered through support tickets.

What documentation is worth writing during takeover?

A short document recording what the system must always guarantee, the flows you traced, the parts nobody understands yet, and the things only one person knew. It is more useful than architectural description because it captures intent, which is exactly the information the code cannot supply.

How do you handle the parts nobody can explain?

Record them explicitly as unknowns rather than pretending they are understood, characterise their current behaviour so changes are detectable, and avoid modifying them until there is a reason. An honest list of unknowns is more useful to the next engineer than a confident diagram that quietly guesses.

Start a project