Migrating Monolithic Legacy Code with AI
A framework migration is one of the highest-stakes software projects a team takes on: months of parallel work, high breakage risk, and the knowledge that every day the old code stays in production is another day it diverges from the new. AI tools do not change the fundamental challenge — but they can cut the translation effort by 30–60% if applied with the right playbook.
Framework migrations are different from ongoing refactoring work in one critical way: they have a hard endpoint. The goal is not to incrementally improve the codebase; it is to move from framework A to framework B and retire framework A entirely. The endpoint changes the risk profile. A partially-migrated codebase running in production — with some modules on the old framework and some on the new — is a liability, not a milestone. Every sprint the migration extends is another sprint of dual-framework maintenance overhead.
AI tools address the most time-consuming part of a framework migration: the mechanical translation work. Converting AngularJS component syntax to React. Updating Java 8 API calls to their Java 17 equivalents. Replacing Rails 5 ActiveRecord patterns with Rails 7 idioms. This translation work is well-defined enough for AI to accelerate it substantially — but it requires a structured playbook to ensure the output is correct, integrated correctly, and retirable cleanly.
The migration-with-AI playbook
The playbook has four steps per migration slice. A "slice" is the smallest independently deployable unit of the migration: in practice, this is usually a single module, service, or feature area that can be migrated and deployed while the rest of the codebase remains on the old framework.
Step 1 — Extract. Before any AI tool runs, a human-led analysis isolates the slice: which files are in scope, what their dependencies are, and which of those dependencies are internal (being migrated) versus external (stable contracts the migration must preserve). The output of the extract step is a file manifest and a dependency boundary specification — the equivalent of drawing a box around the slice and noting every wire that crosses the box boundary. AI tools operate inside the box; the boundary specification is what keeps them from accidentally rewiring the wires.
Step 2 — Translate. With the slice isolated and the boundary defined, an AI agent generates the target-framework equivalent of each component in the slice. The agent receives the source file, the target-framework conventions document, and the boundary specification. The target-framework conventions document is the key input: it specifies not just the new syntax but the new patterns — how errors are handled in the target framework, how state management works, which lifecycle hooks map to which equivalents. Without this document, the AI translates syntax but not patterns, producing code that compiles in the new framework but does not follow the new framework's idioms.
Step 3 — Verify. Every translated component is verified before it is integrated. Verification has three layers: (a) the existing tests that covered the source component pass against the translated component; (b) any missing test coverage is generated and added to reach an acceptable coverage threshold for the slice; (c) behavioural parity checks — automated assertions that the translated component produces the same outputs as the original for a set of representative inputs. This step is where most AI-assisted migrations fall short: teams skip verification to move faster and discover breakage after integration, when the rollback cost is much higher.
Step 4 — Integrate. The verified translated component replaces the original in the deployment. For most migrations, this means a canary deployment: traffic is split between the old component and the new, with monitoring on error rates and response times. If the canary is clean over a defined observation period, traffic is fully switched and the old component is deleted. The deletion step is mandatory — leaving old code alongside new code is the source of the maintenance overhead the migration was supposed to eliminate.
The cycle then repeats for the next slice. Slices are chosen to minimise inter-slice dependencies: if module A depends on module B, migrate B first and A second, so that A's migration can target the already-migrated B rather than having to handle both the old and new versions of B simultaneously.
Step-by-step: a framework migration example
An AngularJS-to-React migration illustrates the playbook at each step. The starting state: a 120,000-line AngularJS application, 340 components, no TypeScript, test coverage below 30%.
Extraction pass: Static analysis identifies component dependencies: which components are leaves (used by others but use nothing themselves) and which are roots (near the top of the UI tree). The migration starts with leaves, which have no downstream dependency on other AngularJS components and can be migrated and wrapped with a React interop layer immediately. React components can coexist with AngularJS components for the duration of the migration via the angular-react bridge — new components are React; they are wrapped as AngularJS directives for the transition period; the wrapping layer is deleted when the parent components are also migrated.
Translation pass: Each leaf component is given to an AI agent with: the source AngularJS component, the team's React style guide (conventions document), the TypeScript type definitions for the data shapes the component handles, and the boundary specification. The agent generates a React functional component, TypeScript props interface, and a test file covering the component's rendering and interaction behaviour. On a 200-line AngularJS component, the translation and test generation takes under two minutes. Manual translation of the same component, to the same quality bar, takes a skilled frontend engineer two to four hours.
Verification pass: The generated React component is tested against the original test suite (adapted to the new component API) plus the AI-generated test additions. Where test coverage was below 30% on the original, the AI-generated additions typically bring it above 70% — not comprehensive, but sufficient for the migration gate. Parity checks compare rendered output between the AngularJS and React versions for the component's representative data shapes.
Integration pass: Verified components ship via the React interop bridge. The AngularJS wrapper is a single configuration line per component; it does not require modifying existing parent templates. Traffic switches are managed at the component level, not the page level, which means the rollback scope for any individual component failure is one component, not a full page.
Real velocity numbers
The numbers below are from the shape of engagements, not specific client data; individual results depend on codebase state, test coverage, and team capacity.
| Migration type | Manual estimate | AI-augmented estimate |
|---|---|---|
| AngularJS → React (120k LOC) | ~20 weeks (2 engineers) | ~10 weeks (same team) |
| Java 8 → Java 17 (80k LOC) | ~6 weeks (2 engineers) | ~3 weeks (same team) |
| Rails 5 → Rails 7 (60k LOC) | ~8 weeks (2 engineers) | ~4 weeks (same team) |
| Python 3.8 → 3.12 (50k LOC) | ~3 weeks (1 engineer) | ~1.5 weeks (same) |
The acceleration comes primarily from the translation and test-generation steps; the extraction, verification, and integration steps retain most of their manual effort because they require judgment about context the AI does not have. A 50% overall time reduction with the same team is a consistent pattern when the playbook is applied in full; skipping the verification step reduces the time estimate but introduces defects that typically cost more to fix than the time saved.
Risk gates
The playbook above handles the mechanical translation well. The risk gates address the failure modes that survive mechanical translation.
Test coverage pre-requisite. Slices with below 40% test coverage should not enter AI-assisted migration until coverage is raised. The AI translation can be correct and still break the existing behaviour if there are no tests to detect the breakage. Raising coverage on the source code, before migration, is a lower-risk investment than trying to validate migration correctness with inadequate coverage. Coverage raising is itself a good AI-assisted task: generating missing tests for well-understood code is a strong AI use case.
Behavioural parity checks. For components that handle data transformations, UI rendering, or API responses, automated behavioural parity checks compare old and new side by side. The check compares outputs for a representative set of inputs: given the same data shape, the old and new components must produce equivalent outputs. Where they differ, the difference is either a bug in the translation (fix it) or an intentional improvement (document it explicitly and update the check to reflect the new expected output).
Canary rollout per slice. Every slice ships as a canary before full traffic switch. The canary window is typically 24–48 hours for internal tooling, 48–72 hours for user-facing surfaces. Monitoring during the canary window covers error rates, response times, and any user-reported regressions. The canary is the production signal that no test suite can fully substitute.
Immediate deletion of retired code. Once a slice's canary passes and traffic is fully switched, the old code is deleted in the same PR that removes the interop wrapper. Leaving old code alongside new code — "just in case" — is a risk that compounds over time: engineers forget which version is live, both versions receive maintenance, and the migration deadline extends indefinitely. The deletion step is non-negotiable in a well-run AI-assisted migration.
When migration is NOT a good AI use case
The 30–60% acceleration claim applies to mechanical translation work. It does not apply to all migration scenarios.
Custom DSL replacement. If the legacy system uses an internal domain-specific language — query syntax, template system, configuration format — that has no direct equivalent in the target framework, the migration requires design work before translation. The AI cannot translate what it does not understand the semantics of. DSL replacement starts with a human-authored specification of the new DSL and its mapping to the old one; only then can AI assist with the mechanical application of that mapping.
Performance-tuned hand-written algorithms. Low-level code that was hand-optimised for specific hardware characteristics — SIMD operations, cache-line-aligned data structures, lock-free queues tuned to a specific core count — does not translate well through an AI that optimises for readability and idiomatic target-language code. The translation may be functionally correct and 40% slower because it lost the performance characteristics of the original. These sections should be flagged before the migration, reviewed by an engineer who understands the performance constraints, and migrated manually with performance benchmarks as the acceptance criteria.
Migrations with no target-framework conventions document. If the team has not agreed on the patterns for the target framework — how to structure components, manage state, handle errors, test interactions — the AI will invent conventions from its training data, producing code that varies in style across different translation sessions. Before AI-assisted migration makes sense, the team needs to agree on and document the target conventions. One week of conventions documentation before the migration saves weeks of style reconciliation during it.
See how a focused engagement approaches legacy migration
The playbook above is the framework. Applying it to a specific codebase — establishing the conventions document, structuring the slice sequence, running the canary gates — is the engagement work.
Legacy Code Refactoring at Wakalix →