Legacy Code Modernization with AI: A Practical Step-by-Step Workflow
Asking an LLM to rewrite a legacy monolith produces confident, plausible, wrong code every time. This workflow shows the test-writing phase most teams skip, and why skipping it sinks the project.

Modernizing legacy code with AI works best as a five-phase loop: inventory and risk-map the codebase, generate characterization tests before touching anything, refactor in small AI-assisted passes validated by those tests, migrate frameworks or languages incrementally behind feature flags, and keep a human reviewer gating every merge. Skipping the test phase is the most common way these projects fail.
Why "just ask the AI to modernize it" doesn't work
Feeding a 40,000-line COBOL or ancient Java monolith into an LLM and asking for a rewrite produces confident, plausible, and frequently wrong output. Context windows are the immediate constraint: even a model advertising a million-token window (like Gemini 1.5/2.0 class models) degrades in retrieval accuracy well before that limit, and most coding assistants chunk files anyway, meaning cross-file behavior (shared mutable state, implicit ordering dependencies, database triggers) gets silently dropped. The second problem is that legacy systems encode business rules nowhere else recorded. A rewrite that "looks cleaner" can quietly delete a tax rounding rule from 2003 that exists because of a customer lawsuit. AI has no way to know that rule matters unless you tell it, and you often don't know either.
The workflow below treats the AI as a fast pattern-matcher and boilerplate generator, not as an architect making judgment calls about what's safe to remove.
Step 1: Build a risk map before writing a single prompt
Before any AI involvement, classify the codebase by blast radius. I use a simple three-tier split:
- Tier A: code that touches money, auth, or regulated data. Changes require full characterization tests and manual review of every diff.
- Tier B: internal tooling, reporting, admin panels. Moderate risk, spot-check AI output.
- Tier C: dead code, unused endpoints, deprecated feature flags. Often the best candidate for outright deletion rather than modernization.
Tools like GitHub's code scanning or simple call-graph analysis (madge for JS, jdeps for Java) help identify which modules are actually load-bearing versus vestigial. In one migration I worked on, roughly 30% of a "critical" billing service turned out to be unreachable code from a pricing model deprecated two years earlier. Deleting it was faster and safer than modernizing it.
How do you get test coverage on code nobody understands?
This is the actual hard problem, and it's where AI is legitimately useful. The technique is called characterization testing (sometimes "golden master" testing): you don't write tests that assert the code is correct, you write tests that assert the code's current behavior, bugs included, so you have a safety net during refactoring.
A practical prompt pattern for an assistant like Claude Code or Cursor:
1Given this function, generate characterization tests that capture its2current behavior exactly as-is, including edge cases and any apparent3bugs. Do not "fix" anything. Use real production-like inputs from the4attached sample data. Flag any behavior that looks unintentional in a5comment, but still assert on it.Run the generated tests against the live (or a snapshotted) system first, not against your mental model of what it should do. I've seen AI generate tests that assert what the code should do rather than what it does, which defeats the purpose. Always diff the AI's assumed behavior against actual captured output (record real request/response pairs in staging, replay them as fixtures).
For batch or scheduled jobs where you cannot easily snapshot, wrap the legacy function and the candidate replacement behind a comparator that logs divergences in production without acting on the new path. This "shadow mode" pattern catches edge cases no test suite anticipated, at the cost of running both code paths (extra CPU and, if either path has side effects like sending emails, extra care to suppress duplicates).
Step 2: Refactor in AI-assisted passes, not one big rewrite
Once characterization tests exist, refactor in narrow, mechanical passes. Each pass should be small enough that a human can review the entire diff in one sitting, typically under 400 lines of actual logic change (generated boilerplate can be larger).
Good candidates for AI-driven mechanical passes:
- Renaming and extracting functions without behavior change
- Converting callback pyramids to async/await
- Replacing hand-rolled date or string utilities with standard library equivalents
- Adding type annotations to untyped Python or JavaScript
Bad candidates for a single AI pass:
- Changing the concurrency model (thread pool to async event loop)
- Swapping an ORM or database driver
- Restructuring class hierarchies that other services depend on via reflection
For the "bad candidate" category, break it into a sequence: introduce an adapter layer first, run both implementations in parallel with the shadow-mode comparator, then cut over, then remove the adapter. This costs more calendar time but avoids the failure mode where a single 3,000-line AI-generated diff gets rubber-stamped because nobody has the patience to review it line by line. That rubber-stamping is the single biggest source of AI-introduced regressions I've seen in production incidents; the code compiles, tests pass because the tests were also AI-generated for the new behavior, and the actual bug ships.
What's the right model and context strategy for large legacy codebases?
For codebases beyond a few thousand lines, retrieval matters more than raw context window size. Options in order of setup cost:
- IDE-integrated agents (Cursor, GitHub Copilot Workspace, Windsurf) that index the repo and pull relevant files automatically. Lowest setup cost, works well for Tier B/C code, weaker on cross-service business logic.
- Explicit context curation: you manually assemble the relevant files, schema definitions, and a written summary of business rules into the prompt. Higher effort, much better accuracy for Tier A code. This is where a model with a large context window (Claude's 200K token context or Gemini's larger windows) earns its cost, because you're not relying on automatic retrieval to guess what's relevant.
- Agentic multi-step workflows (Claude Code, OpenAI's Codex-based agents) that can run tests, read failures, and iterate. Useful for Tier B and mechanical Tier A changes, but every iteration costs tokens and wall-clock time. A single "refactor this module and fix any failing tests" agent run against a mid-size module can take several minutes and multiple dollars in API spend if it loops through failures a few times. Budget for this; it is not free exploration.
For strictly regulated code (financial calculations, HIPAA-adjacent PHI handling), I still hand-curate context rather than trusting automatic retrieval, because a missed file in the index means a missed constraint, and there's no error message for "the AI didn't know about a rule that existed in a file it didn't retrieve."
Step 3: Migrate frameworks incrementally with the strangler pattern
For framework or language migrations (Python 2 to 3, AngularJS to React, monolith to services), the strangler fig pattern remains the safest structure, AI or not. You route a subset of traffic or a subset of features to the new implementation while the old one keeps running, and expand the new surface area over time. Martin Fowler's writeup of this pattern predates the current AI tooling wave, and nothing about LLM-assisted coding changes the underlying risk calculus: you still want a rollback path that doesn't require a redeploy.
Where AI genuinely accelerates this: generating the translation layer between old and new data models, writing the boilerplate for parallel-run comparators, and producing first-draft ports of well-isolated modules (a pure calculation function, a formatting utility). Where it doesn't help much: deciding the cutover sequence, which is a product and risk decision, not a code-generation task.
Step 4: Gate everything through human review, but change what review looks for
When AI generates a large share of the diff, human review has to shift focus. Reviewing for style and syntax is largely wasted effort now (the AI's style is usually fine). Review should concentrate on:
- Does this diff delete or alter any conditional that isn't covered by a characterization test?
- Does the AI's summary of "why" match what the business logic actually implies, or is it a plausible-sounding guess?
- Are there new dependencies or API calls introduced that weren't in the original (AI sometimes "helpfully" adds error handling that changes retry behavior or timeout semantics)?
Tools that surface diff-level risk (GitHub's built-in review tools, or CI checks that flag changes to files without corresponding test changes) are worth wiring into the pipeline as a hard gate for Tier A code, not just a suggestion.
Cost and timeline realities worth planning for
Teams consistently underestimate two things: the token cost of iterative agentic refactoring on large files, and the calendar time characterization testing adds up front. On a real migration of a mid-size Rails monolith (roughly 80,000 lines) to a partially extracted service architecture, the characterization test-writing phase took about three weeks with two engineers and heavy AI assistance, before any actual modernization code was written. That felt slow in the moment and was the reason the eventual migration shipped without a production incident. Teams that skip straight to rewriting typically ship faster initially and then spend longer in incident response afterward.
A workflow to actually run
Start any legacy modernization engagement with a two-day spike: risk-map the codebase, pick one Tier B module as a pilot, and run the full loop (characterization tests, small AI-assisted refactor, shadow-mode comparison, human review) end to end before committing to a timeline for the rest of the system. If the pilot module takes longer than expected because the AI keeps missing an implicit dependency, that's your signal to slow down and hand-curate context for the rest of the project rather than trusting automatic retrieval. If the pilot goes smoothly, you've validated the tool chain and can scale the same loop across Tier B and C code quickly, reserving your slowest, most manual version of this process for the Tier A code that actually deserves it.
Related Articles

Debugging with AI: A Structured Problem-Solving Framework for Production Issues
Pasting a stack trace and asking 'what's wrong' gets you the statistically likely cause, not the actual one in your system. This framework treats the model's first answer as a hypothesis, not a diagnosis.

From Spec to Suite: An AI-Powered Test Generation Workflow That Actually Works
AI-generated suites can hit 90% line coverage and still miss the regression that ships two weeks later. Here's the workflow that extracts real requirements from the spec before any code exists.

Quantifying AI Coding Impact: Metrics Beyond Lines of Code
A 40% jump in 'AI-authored lines' looks like a win right up until median PR review time doubles. Here's what to measure instead of the vanity metrics vendors default to.