Every company has one: the app nobody wants to touch. It runs on a framework that stopped getting updates years ago, the person who wrote it has left and every change takes three times longer than it should. AI coding tools have made migrating that kind of system much cheaper, but only if they are used inside a process that catches their mistakes. This is how AI-assisted legacy code migration works when it is done carefully, and where it goes wrong when it is not.
Why the big rewrite usually fails
The tempting plan is to rewrite the whole thing in a modern stack and switch over on one day. It rarely works. The old system contains years of small decisions: edge cases, workarounds for a customer, a rounding rule someone added after an invoice dispute. Most of those decisions are not written down anywhere except the code. A clean rewrite drops them, and you find out one bug report at a time after launch.
AI makes this temptation stronger, because a model can generate a new codebase quickly. Fast output does not change the underlying problem. If you cannot prove the new code behaves like the old code, you have just produced a larger amount of unverified software.
Step 1: Lock in current behaviour with characterization tests
Before changing anything, write tests that describe what the system does today. Not what it should do, what it actually does. These are called characterization tests. They are the foundation of everything that follows.
- Record real inputs and outputs: API requests and responses, report outputs, files the system generates.
- Cover the paths that make money or touch customers first: billing, checkout, permissions, data exports.
- Keep the odd behaviour in the tests, even when it looks like a bug. Decide later, on purpose, whether to change it.
- Make the tests runnable against both the old and the new code.
This is a good first job for AI. Given a module and a set of recorded inputs, a model can draft many tests quickly. An engineer still reviews them, because a test that asserts the wrong thing gives false confidence.
Step 2: Map the system before you move it
Build a map of modules, dependencies, data stores, external integrations and scheduled jobs. Note which parts change often and which have not been touched in years. AI tools are useful here too. They can read a large codebase, summarize what each module does and point out where a function is called from. Treat those summaries as a starting point to check, not as documentation.
The map tells you where the seams are. Seams are the places where you can cut one piece out and replace it without touching everything else: a service boundary, an API, a background job, a single page.
Step 3: Migrate incrementally with the strangler fig pattern
The strangler fig pattern replaces a legacy system piece by piece. New code grows around the old system and takes over one route, feature or job at a time, until nothing is left of the original. At every point, the system works and can ship.
- Put a routing layer in front of the old system (a proxy, an API gateway or a router inside the app).
- Pick one slice with clear boundaries and good test coverage.
- Build the new version of that slice.
- Run both side by side where you can and compare outputs.
- Route traffic to the new slice, behind a feature flag so you can switch back.
- Remove the old code for that slice once it has run cleanly for a while.
Small steps are what make AI safe to use. A model drafting a change to one module, checked by a test suite and reviewed in a normal pull request, is a very different risk from a model rewriting a whole application at once.
Step 4: AI drafts, engineers review
In practice, the split of work looks like this:
| Task | AI does | Engineer does |
|---|---|---|
| Characterization tests | Drafts tests from recorded inputs and outputs | Checks assertions, adds missing edge cases |
| Code translation | Converts a module to the new framework or language | Reviews every diff, checks behaviour against tests |
| Dependency upgrades | Finds deprecated calls and proposes replacements | Reads changelogs, decides on breaking changes |
| Documentation | Summarizes modules and writes first drafts | Corrects and approves |
| Architecture | Suggests options | Decides |
The review is not optional. Models produce code that looks right and compiles but changes behaviour in small ways: a different default, a missing null check, an error that used to be raised and is now swallowed. They also call library functions that do not exist in the version you are using. The test suite catches some of this. Careful human review catches the rest.
Common migration types
Framework upgrades
Moving to a new major version of the same framework, or from one framework to another in the same language (for example, an old frontend framework to React). These are good fits for AI-assisted work because the changes are repetitive and well documented. Upgrade in stages. Jumping several major versions in one step multiplies the breakages you have to untangle at once.
Language upgrades and moves
Older language versions to current ones (Python 2 to 3, older Java or .NET versions to current releases) or moving between languages. The risk is in subtle semantic differences: string handling, integer division, date and time behaviour, concurrency. Characterization tests around these areas matter more than anywhere else.
Monolith to services
Splitting a large application into smaller services. Do it only where there is a clear reason, such as a part that needs to scale or deploy separately. Splitting for its own sake adds network calls and failure modes without much benefit.
Scripts and glue code
Cron jobs, spreadsheets with macros and one-off scripts that quietly run part of the business. These are often the riskiest pieces because nobody remembers they exist. Find them during the mapping step. If the data they touch is moving too, plan it alongside the data migration.
Pitfalls to avoid
- Migrating without tests. You will not know what you broke.
- Accepting large AI diffs. Keep changes small enough that a reviewer can actually read them.
- Fixing bugs during the move. Migrate first, then fix. Mixing the two makes it impossible to tell whether a difference is a regression or an improvement.
- Forgetting the data. Schema changes and data moves need their own plan and their own checks.
- No way back. Every slice should be switchable back to the old code until it has proven itself.
How Remqa runs code migrations
Our forward deployed engineers run legacy code migrations inside your repo and your release process. Tests come first to lock in current behaviour. AI drafts the changes, an engineer reviews every one and the work ships in small stages behind switches you control. Same process as every migration we run: map, test run, verify, cut over, with the old system kept available until you sign off. If you are moving business systems at the same time, see the CRM migration checklist. Read more about how we handle code migration.