How to Use AI to Migrate a Legacy Codebase
Migration means moving a codebase to a new language, framework, or platform, not cleaning it up in place. Here is a checklist for doing it with an AI coding agent without losing undocumented business logic.
How to Use AI to Migrate a Legacy Codebase
Migration and refactoring get used as if they are the same job, and they are not. A refactor changes how code is written without changing what it runs on: same language, same framework, same platform, cleaner internals, identical behavior. A migration changes the ground the code stands on: a different language, framework, or platform entirely, jQuery to React, PHP to Node, Python 2 to Python 3, a monolith split into services. That distinction changes almost every decision that follows. If what you actually want is to clean up code staying on its current stack, how to use AI to refactor legacy code is the companion piece. This one is about moving a codebase somewhere else entirely.
AI coding agents make migration faster because they translate syntax quickly, but speed on the wrong plan just gets you to a broken state faster. The real risk in any migration, AI-assisted or not, is losing behavior nobody wrote down. A legacy codebase encodes years of business logic and workarounds that live only in what the code does, not in what anyone can explain. A migration checklist has to start there, inventorying real behavior, before a single line gets translated.
Migration vs refactor: the distinction that decides your plan
The two get conflated because both start from ugly, old code and both use an AI agent to improve it. A refactor keeps the contract between the code and the world the same and only changes the implementation. A migration changes the contract by definition: a different language has different type coercion, a different framework has different lifecycle conventions, a different platform has different defaults for timeouts and concurrency. Treating a migration like a big refactor is where teams get hurt, because they carry over an assumption that the target runtime will behave identically to the source, into a project where that assumption is false by construction.
Refactor | Migration | |
|---|---|---|
What changes | Implementation only | Language, framework, or platform |
What stays fixed | Language, framework, platform | Nothing is guaranteed to carry over |
Main risk | A regression in existing behavior | A regression plus semantic drift between stacks |
Safety net | Characterization tests on one codebase | Characterization tests plus a parallel run across both |
Step 1: Inventory what the legacy code actually does, not what it looks like
Reading the old code tells you what it looks like. It does not reliably tell you what it does in production, and that gap is where migrations lose business logic. Start with behavior, not source.
Capture real inputs and outputs before touching anything. Log a sample of real production traffic, database rows, or batch inputs and their actual outputs. This is your ground truth; documentation is not a substitute for it.
Ask an AI agent to produce a behavior inventory of the legacy module: every input it accepts, every branch, every external call, every error path. How to get an AI coding agent to explain a legacy codebase covers doing this chunk by chunk instead of a whole-file summary that glosses over edge cases.
Cross-check the inventory against the captured traffic. An agent reading source code describes what the code appears to do. It will not know a branch only fires for one legacy client ID unless that shows up in real captured inputs.
Write characterization tests on the current stack from that captured behavior, bugs included. These become the same-language oracle you compare the migrated version against, and they matter more here than in an in-place refactor, since you cannot eyeball two languages side by side and tell if they agree.
List every consumer: other services, cron jobs, scheduled reports, admin scripts. Legacy systems accumulate silent dependents, and a migration that misses one breaks something nobody was watching.
Step 2: Pick your migration shape, strangler fig or big bang
A strangler fig migration routes traffic through a facade and moves functionality behind it piece by piece, so old and new run side by side until the old system has nothing left to do. A big-bang migration replaces the whole system at once. AI agents make both faster, but do not change which one is the safer default.
Strangler fig | Big bang | |
|---|---|---|
Risk lands | One slice at a time | All at once, at cutover |
Rollback | Revert one slice | Revert the whole migration |
Timeline | Longer overall, ships continuously | Shorter if it works |
Fits best | Live systems with real users, codebases too large for one pass | Small, bounded, low-traffic systems already slated for replacement |
Default to strangler fig for anything customer-facing or revenue-touching. Reserve big bang for genuinely small, self-contained pieces where the blast radius of getting it wrong is low and the entire scope fits inside what a human can review in one sitting.
Step 3: Translate chunk by chunk with test coverage as the safety net
This is where the AI agent actually does the migration work, and where the checklist matters most, because a chunk that skips a step here is exactly how undocumented behavior disappears.
Pick the smallest chunk with a clear boundary: one endpoint, one module, one component, something with its own characterization tests from step 1. How to split a big task for an AI coding agent covers sizing this correctly so the agent, and you, can actually hold the whole change in view.
Give the agent the behavior inventory and captured traffic alongside the source, not just an instruction to rewrite the file. "Translate this" produces idiomatic-looking code that quietly drops edge cases. "Translate this so every case in the attached behavior inventory still produces the attached output" gives the agent something to be graded against.
Run the same captured inputs through both versions and diff the outputs, not just the migrated version's own tests. Green tests only prove the new code agrees with itself. A parallel diff against the old system's real outputs is what catches semantic drift.
Treat every discrepancy as a decision, not noise. Each mismatch is either a bug you are knowingly fixing or a bug you just introduced. Decide which, explicitly, before moving on. Accepting a discrepancy because the new output "looks more correct" is how migrations change behavior nobody asked to change.
Ship the chunk behind a flag and shadow it on real traffic before cutting over, especially anything customer-facing. Compare live outputs for a period, then repeat with the next chunk.
Common failure modes in AI-assisted migrations
Losing undocumented business logic. An agent translates the code it can see. It has no way to know a stray conditional exists because a customer once disputed a charge and someone patched around it. This is why step 1's inventory has to come from real captured behavior, not a read-through, and why a domain expert should review anything the agent flags as unclear.
Silent behavior changes from platform semantics. Floating-point rounding, date and timezone handling, string encoding, how null, undefined, and empty string are treated, sort stability, and case sensitivity in comparisons all differ across languages and frameworks by default. Code that translates cleanly and passes a quick smoke test can still diverge on exactly these edges, which is why a parallel diff on real inputs matters more than a green test suite.
Confidently wrong translations. An agent will produce code that compiles, looks idiomatic, and is simply wrong about implicit type coercion or default argument behavior from the source language. It will not flag this as uncertain, because from the model's perspective it just wrote normal-looking code.
Skipping data-layer validation. Teams migrate application code carefully and wave the data layer through, assuming a new ORM or driver reads the same rows the same way. Its null handling, timestamp parsing, or enum mapping can quietly diverge from the old one even when the database itself has not changed. Did your AI coding agent actually run the tests? covers a related failure: an agent reporting a chunk as verified when the tests it ran never covered the path that broke.
When big bang is actually the right call
Strangler fig is the safer default, not a universal rule. A small internal tool with a handful of users, or a system where running two versions in parallel is more complex than the migration itself, is a case where a clean rewrite from a clear behavior spec can beat weeks of scaffolding. The deciding factor is blast radius: low-traffic systems tolerate a bigger single move. Anything touching money, auth, or a large user base does not.
A worked example
A legacy PHP endpoint calculates shipping rates and is called by three different clients. The team is migrating it to a Node and TypeScript service as part of a broader move off the PHP monolith.
Capture a week of real requests and responses from the PHP endpoint, across all three calling clients, as ground truth.
Ask the agent for a behavior inventory of the PHP function: every input field, every branch (free shipping thresholds, international surcharges, weight bands), rounding rules, and error responses.
Write characterization tests in PHP first and confirm they pass against the current endpoint, using the captured traffic as cases.
Translate to Node and TypeScript using the behavior inventory and captured traffic as the spec, not the PHP source alone.
Shadow the new endpoint on production traffic for a week, diffing every response against the PHP version before serving Node output to real users.
The diff turns up a one-cent discrepancy on roughly one in twenty orders: PHP truncates the final rate, the Node translation rounds. Nothing in the comments mentioned truncation. Without the parallel diff, it would have shipped silently and surfaced weeks later as a finance reconciliation mismatch.
That last step is the whole argument for this checklist. The bug was not really in the agent's code, in isolation either version is a reasonable way to round a number. The bug was in migrating without a mechanism that could catch a legitimate-looking discrepancy before it reached production.
For the broader set of habits this fits into, see AI coding tools. And for the in-place counterpart, how to use AI to refactor legacy code covers keeping a codebase on its current stack while making it easier to work in.
Frequently asked questions
How is migrating with AI different from refactoring with AI?
Refactoring keeps the language, framework, and platform fixed and only changes the implementation. Migration changes at least one of those, so the target code can legitimately behave differently by default, and you need a way to compare behavior across two stacks, not just review a diff within one.
Can an AI agent migrate an entire codebase in one pass?
For a very small, self-contained codebase, maybe, and a full behavior diff afterward is still worth doing. For anything of real size, a single pass exceeds what any reviewer, human or model, can verify at once. Chunk it and verify each chunk before moving on.
How do I handle legacy code that has no tests at all?
Capture real inputs and outputs from production before writing anything, then have the agent generate characterization tests from that captured behavior on the current stack. Those tests, not the source code itself, become the spec the migrated version has to match.
How do I know a migrated chunk is safe to fully cut over?
Run it in shadow or parallel against real production traffic long enough to see your actual edge cases, not just a synthetic test run, and require the diff against the old system's output to be clean, or every discrepancy explicitly explained, before removing the old path.
How did this land?
About the author

Developer Advocate
Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.


