The developer who built the system left two years ago. The code works, customers pay through it, and nobody touches it out of fear. Then a small change request comes in, and the team asks for a week just to understand where to make it. This is the point where you decide what to do with the system.
The common reaction is "let's rewrite everything." That is usually riskier than the system itself. The approach that generally works is to regain control in stages, where every stage starts with understanding and not with change.
First, make sure you can go back
Before you touch the code, you need to be able to restore the system. That means a backup of the code, the database and the files the system generates, plus a check that the restore really works on a clean machine. A backup that hasn't been tested is an assumption.
Also collect what isn't in the code: environment variables, keys to external services, scheduled jobs on the server (cron), DNS settings and certificates. In old systems, part of the business logic lives outside the repository, and that is where the surprises hide.
A test environment that resembles production
The next goal is to run a copy of the system in a separate environment, with anonymized or masked data. Without it, every test is an experiment on customers.
This can be exhausting: a PHP or Node version that is no longer supported, libraries that were removed, dependence on a specific server. You can handle it with a container that pins the old versions. The goal at this stage is for the system to work as it is, and being up to date can wait.
The first time you run an old system in a separate environment, something undocumented almost always turns up: a scheduled job that runs at night, a file that arrives from a shared folder, or an external service the system quietly calls. Every such discovery goes into the work plan before you touch the code, because it is exactly the thing that would have broken during the migration.
Document what the system does, not what it is supposed to do
When there is no spec, the existing behavior is the spec. The way to capture it is characterization tests: tests that run the system with real inputs and record what comes out. They check whether the answer changed, without deciding whether it is correct.
Start with the processes that touch money, customer data and integrations, because the damage is greatest there. Twenty tests on the critical paths give the team the confidence to make changes, and serve as a safety net for every upgrade that follows.
At the same time, add logging and monitoring. Once you can see which parts are actually called and which haven't been touched in years, you can stop fearing the dead code.
Risk mapping: where to start
Not every part of the system deserves the same attention. Rate each module on two axes: how much the business depends on it, and how recently it was touched. A critical module that hasn't changed in years is a candidate for characterization tests and a freeze. A critical module that changes every month is the place to invest in the first upgrade. A marginal module nobody uses can wait for later, and may be deleted.
It is also important to list the external risks: an operating system or language version that has dropped out of security support, libraries with known vulnerabilities, a third-party service that plans to shut down. These determine urgency, and they are the right argument with management that doesn't want to invest in something that "works."
Gradual upgrade instead of a rewrite
A full rewrite guarantees two systems running in parallel for a long period, and a new system that must reproduce every behavior of the old one, including bugs someone has gotten used to. That's why such projects tend to drag on.
The common and safer approach is the strangler pattern: you build the new part next to the old one, route one function at a time to it, and once the old one is no longer in use you delete it. The recommended order:
- Upgrade language and dependency versions step by step, running the tests after every step.
- Separate one especially painful part, such as billing or notifications, into its own service or module.
- Replace the user interface, if needed, once the server is stable and has a clear API.
- Remove unused code at the end, after monitoring has proven it is dead.
Each stage ships on its own and can be rolled back. If something goes wrong, you go back just one step.
When you should rewrite
There are situations where a gradual upgrade doesn't pay off: a technology with no people available in the market, an architecture that blocks every required change such as multi-tenancy or growth in load, or a system small enough that rebuilding is cheaper than understanding it. Even then, it's worth using the earlier steps: you can't build a replacement for something when you haven't understood how it works.
In any case, you make this decision after a review of the system has been done, not before it. If the rewrite gets a green light, we recommend starting with a replacement MVP and comparing it against the existing system, as described in how small an MVP should be.
Estimation and accountability
An estimate for a project like this will always be a range, and it is accurate only after the review stage. So it's better to price the review by itself, with a document at the end that gives a snapshot: what exists, what is risky, what the recommendation is and in what order. Anyone who commits to a full schedule before seeing the code is guessing.
It's also worth deciding who is responsible for the system during the upgrade. An old system with no owner stays that way. You need one person on the business side who makes decisions, and a team responsible for the code, so that change requests don't bypass the process.
What an external team can contribute
An old system is work that internal teams tend to put off, because it is less glamorous than a new product and because nobody wants to be responsible when it breaks. An external team with years of experience can take on the review and the first stages while your team continues with ongoing development.
The condition is an organized knowledge transfer: your team takes part in the reviews, receives the documentation and knows the tests. That way, when the stage ends, the system is yours and not the vendor's.
Organizing knowledge: the documentation worth writing first
After every stage, update one short document: a diagram of the components and the connections between them, a list of external settings, and the rules for "how we release a version." It doesn't need to be pretty. It needs to be accurate, and any new developer should be able to read it on day one.
It matters that the knowledge doesn't stay with one person. That is what led to the situation you are in: a system nobody understands. If the team that maintains it changes, the document is what keeps the understanding in-house.
The bottom line
You upgrade an old system in a fixed order: first backup and restore, then a test environment, tests that capture existing behavior, and only then small changes that can be rolled back. A rewrite is reserved for cases where there is no alternative, and is never the starting point.
If you don't have an internal team that can take this on, we assemble a team and commit to timelines against a work plan in our Outsourcing track.





