Back to Blog
Taking Over a Legacy Codebase You Didn't Write

Taking Over a Legacy Codebase You Didn't Write

May 31, 2026
Stefan Mentovic
legacy-coderefactoringcharacterization-testingtechnical-debttesting

Inheriting a live, business-critical app from a previous team is mostly risk management. Here is the measured approach we use to take over legacy code safely.

At some point the email arrives. A business is running on software the people who built it left long ago. There are no tests, no documentation, and no one to ask why anything works the way it does. It still processes real orders every day. And the question on the table is the hardest one in our field: how long until it is safe to change?

Answering that honestly has less to do with programming talent than with method. Taking over someone else's live codebase is a risk-management problem first and an engineering problem second. Here is how we approach it without breaking the thing that pays the bills.

#What "inherited" really means

Inheriting a codebase means inheriting decisions whose reasons are gone. A previous team, often one the client no longer trusts, made thousands of choices that now live only in the code. Some were deliberate. Some were shortcuts taken at midnight before a deadline. From the outside they look identical.

The hardest part is that behavior which looks like a bug is sometimes load-bearing. A customer has quietly built a workflow around it. A downstream report depends on the exact number it produces. Delete it as obviously wrong and you break something nobody wrote down. And because there are no tests, there is no definition of "working" to fall back on, so every change is a guess with money attached.

Layered on top is a human problem. The knowledge that should have been documented walked out the door with the developers who held it. When those developers were also unreliable, the client tends to arrive wary, sometimes having already paid twice for the same promise. Your first job in that situation is not code at all. It is demonstrating that this time is different.

#The business is the constraint, not the code

It is tempting to treat a messy codebase as a purely technical problem, solved with a clean rewrite. That framing is exactly how takeovers fail. The constraint is never the code. It is the business running on it.

The application earns revenue while you work on it. Its users do not care that the internals are ugly; they care that checkout still works on Monday. A change that would be routine on a greenfield project carries real financial weight here, because the downside of a mistake is counted in lost orders and lost trust, not in a red test on your laptop.

That reality sets the rules before a single decision about frameworks or architecture. No big-bang rewrite that goes dark for three months. No clever refactor that cannot be undone. Every step has to leave the business at least as functional as it found it, and ideally a little safer than before.

#Do not rewrite. First, pin down what it does.

Before changing a line, we make the system tell us what it currently does, and we record that in the one language that does not lie: tests.

This is characterization testing, sometimes called golden-master testing. You take a piece of inherited code, feed it representative inputs, and capture the outputs exactly as they are today, including the strange ones. Consider a shipping calculator we might inherit:

// Inherited. No tests, no docs, no author left to ask.
final class LegacyShipping
{
    public static function cost(array $order): int
    {
        $base = 750;

        if ($order['country'] !== 'US') {
            $base = 2500;

            if (($order['weight'] ?? 0) > 5) {
                $base += 1000;
            }
        }

        if ($order['country'] === 'US' && ($order['subtotal'] ?? 0) >= 5000) {
            return 0;
        }

        if (($order['coupon'] ?? null) === 'SHIPFREE') {
            return 0;
        }

        return $base;
    }
}

There are undocumented rules buried in there: free shipping for US orders over fifty dollars, a flat international rate with a weight surcharge above five kilograms, and a coupon that overrides everything. Are they all intentional? We do not know yet, and that is precisely the point. We capture the behavior first and judge it later. A Pest test does the capturing:

it('reproduces the shipping cost the legacy system already produces', function (array $order, int $expected) {
    expect(LegacyShipping::cost($order))->toBe($expected);
})->with([
    'US small order'         => [['country' => 'US', 'weight' => 2, 'subtotal' => 3000], 750],
    'US free over $50'       => [['country' => 'US', 'weight' => 2, 'subtotal' => 8000], 0],
    'international base rate' => [['country' => 'DE', 'weight' => 2, 'subtotal' => 8000], 2500],
    'international heavy'     => [['country' => 'DE', 'weight' => 9, 'subtotal' => 8000], 3500],
    'coupon overrides all'   => [['country' => 'DE', 'weight' => 9, 'coupon' => 'SHIPFREE'], 0],
]);

These tests assert nothing about whether the logic is correct. They assert what the legacy system already does. With them in place the code stops being a black box. We can now refactor it, extract it into a proper service, or reimplement it from scratch, and the instant one of these goes red we have either uncovered a real bug worth a conversation with the client or introduced one worth fixing immediately. The safety net comes first. The cleanup comes second.

#Estimate the state before you estimate the effort

The question we opened with, how long will this take, cannot be answered until a different question is answered first: what state is this actually in?

So we run two estimates, in order. The first is an honest assessment of the system as it really runs: where the complexity hides, which parts carry genuine business logic, what is healthy and what is held together with tape. Only with that picture can the second estimate, the effort to change or rebuild, rest on evidence instead of hope. Quoting a timeline before assessing the state is not optimism, it is how projects quietly blow their budget in week three. We dug into that sequencing in the Submitit platform case study, where a careful read of the inherited system is what made committing to a tight timeline responsible rather than reckless.

#Rebuilding trust is part of the engineering

When a client has been let down by the team before you, technical competence is necessary but not sufficient. They need to see that the work is under control.

We do that with transparency rather than promises. Small, verifiable wins early. Honest naming of the unknowns instead of confident hand-waving over them. A steady rhythm of "here is what changed, and here is the test that proves the rest still works." None of this is glamorous, and all of it is engineering, because trust is what buys the room to do the deeper work later. A client who can see the safety net is a client who will let you rebuild the engine underneath it.

#When your software has you trapped

A lot of businesses do not feel like they own their software. They feel owned by it. The code is opaque, the only people who claim to understand it are the ones who wrote it, every change comes back with a high quote and a slipping timeline, and switching providers feels more dangerous than staying put. That is not technical lock-in so much as knowledge lock-in, and it is often the direct result of a previous team never writing anything down, whether by neglect or by design.

The measured takeover described above is the way out of exactly that trap. Once we capture the existing behavior in tests, map the architecture, and turn implicit rules into explicit, checked ones, the system becomes legible again. The business stops depending on any one person's memory, because the knowledge now lives in the codebase where anyone competent can pick it up. That is what optionality looks like: the freedom to extend it, rebuild it, or even change vendors, on your terms rather than under pressure.

None of this is unfamiliar territory for us. Inheriting undocumented, business-critical systems from teams that have left or underdelivered, stabilizing them without downtime, and handing control back to the owner is a core part of what we do. The approach is the same every time: reduce the risk first, earn the room to improve, and leave the client holding the keys. And the first step is deliberately small. A focused assessment of your system's real state is low-cost and low-risk, and it gives you an honest picture before you commit to anything. You should never have to bet the business just to find out where you stand.

#Carried into the Submitit rebuild

We carried this approach into the Submitit rebuild, inheriting a live submission platform that had to keep serving paying customers the entire time. Capturing the existing behavior in tests before touching it is exactly what let the proprietary matching logic survive the move intact. The full story is in the Submitit case study.

A takeover is also where the surrounding decisions get made. Once you understand the true state, you can make a sober call on the right tech stack and how much to rebuild, put real discipline into testing the logic the business depends on, and bring the test and production environments up to a standard the previous team never set.

#Discipline beats brilliance

Taking over a legacy application is not about being the smartest person in the room. It is about being the most disciplined. Assess the true state before you promise anything. Pin down current behavior with characterization tests before you change it. Keep the business running every single day. And earn back the trust the last team spent.

If you have inherited software that runs your business but nobody fully understands anymore, that is precisely the situation we like to walk into. Let's talk.