AI-Assisted Legacy Code Migration: What Actually Works
Language models can translate code quickly. Migrating a system safely is a different job. Here is how we combine the two without betting the business on it.
Every company older than a few years has at least one system nobody wants to touch. It runs on a framework that stopped receiving updates, the people who wrote it have moved on, and every change takes three times longer than it should. Moving it onto a modern stack has always been expensive, mostly because someone has to read and rewrite every line by hand.
Language models change part of that equation. They are fast at reading unfamiliar code and producing a plausible translation into another language or framework. The word that matters there is plausible. This post covers where AI genuinely helps in a migration, where it introduces risk, and the process we follow so that speed does not come at the cost of correctness.
Translation is the easy part
A migration is not a translation exercise. The hard parts of moving a legacy system are understanding what it actually does (which is often different from what the documentation says), preserving behaviour that users rely on without knowing it, and cutting over without downtime or data loss. Translating syntax is perhaps a fifth of the work.
That is why teams who point a model at a repository and ask it to rewrite everything in TypeScript tend to end up with a codebase that compiles, looks tidy and behaves subtly differently. Rounding rules change. Date handling shifts by a timezone. An error that used to be swallowed now propagates. None of this is visible in a code review of the output alone.
It also helps to be honest about why the system is being migrated. If the goal is to escape an unsupported runtime, a faithful port is the right target. If the goal is to change how the business works, a port is the wrong target and you are really looking at a rebuild with a migration path for data. Mixing the two goals in one project is one of the most common reasons modernisation work overruns.
Where AI earns its place
Used inside a disciplined process, models are genuinely useful at several stages:
- Code archaeology. Summarising modules, tracing call paths and explaining what an undocumented function does. This shortens the discovery phase considerably.
- Generating characterisation tests. Proposing test cases that pin down current behaviour, including edge cases a human might not think to check.
- Mechanical translation. Converting data models, DTOs, validation rules and straightforward business logic module by module.
- Boilerplate on the new side. Scaffolding controllers, repositories and API clients that follow the conventions of the target stack.
- Reviewing diffs. Acting as a second reader that flags suspicious behaviour changes between old and new implementations.
In each case a human engineer owns the result. The model drafts; the engineer verifies, corrects and commits.
Give the model the right context
Output quality depends heavily on what the model sees. Feeding it one file at a time produces code that ignores the conventions of the target codebase. We give it the relevant types, an example of an already-migrated module, the coding standards and the characterisation tests for the slice. The result needs far less correction, and reviewers can compare like with like.
Tests first, translation second
The single most important rule in any migration, AI-assisted or not, is that you need a way to prove the new system behaves like the old one. For legacy code that usually means writing characterisation tests before changing anything: tests that record what the system currently does, right or wrong, so you can detect any difference.
- Identify the module boundaries and pick a first slice with clear inputs and outputs.
- Capture real (anonymised) inputs from production logs or fixtures and record the old system's outputs.
- Use a model to propose additional edge-case inputs, then record the old outputs for those too.
- Translate the slice, with the model drafting and an engineer reviewing.
- Run both implementations against the same inputs and diff the results. Investigate every mismatch.
- Only when the diff is clean, or every difference is a deliberate fix, does the slice move forward.
This sounds slow. In practice, AI makes the test-generation step much faster, which is exactly where manual migrations used to cut corners.
Migrate in slices, not in one big bang
We almost never recommend a full rewrite followed by a single cutover. The strangler pattern works better: put a routing layer in front of the old system, move one capability at a time to the new service, and send traffic to it gradually. If something goes wrong, you route back. The old system shrinks until it can be switched off.
For data, the same principle applies. Run both stores in parallel for a period, reconcile them, and keep a rollback path until you are confident. We describe the backend patterns we tend to land on in how we structure NestJS, Prisma and PostgreSQL backends.
Choosing the first slice
The first slice should be valuable enough to matter and small enough to finish in a few weeks. Good candidates are read-heavy capabilities with clear inputs and outputs, such as a pricing calculation, a report or a lookup API. Avoid starting with the module that writes to every table. A successful first slice proves the pipeline end to end: tests, translation, review, deployment, routing and rollback.
Data deserves its own plan. Schema changes, encoding differences and years of inconsistent records surface during migration, not before. Profile the data early, write reconciliation checks that compare record counts and key totals between old and new stores, and rehearse the cutover on a copy of production data more than once.
Risks to plan for
- Confident errors. Models rarely say they are unsure. Treat every generated line as unreviewed until proven otherwise.
- Licensing and confidentiality. Decide up front which code may be sent to which model provider, and consider a self-hosted model for sensitive repositories.
- Lost intent. A faithful translation of a bad design is still a bad design. Use the migration to fix structure, but separate refactoring commits from behaviour changes.
- Hidden dependencies. Cron jobs, stored procedures, shared database tables and file drops are easy to miss. Inventory them before you start.
- Team knowledge. If the people who will run the new system did not take part in the migration, you have created a new legacy system.
How to tell if it is working
Good signals are boring ones: the behavioural diff stays clean, each slice ships to production on its own, incident rates do not rise during cutover, and engineers can make changes in the new code faster than they could in the old. If the team is debating why outputs differ late in the project, the tests came too late.
Planning a migration?
We help teams modernise systems they depend on, combining AI-assisted tooling with careful, test-led engineering. See our custom software development service, or get in touch to talk through the system you need to move.