Legacy Code With AI: How to Document and Migrate Safely

Legacy Code With AI: How to Document and Migrate Safely

A four-step plan for using AI on legacy code: map, document, write characterization tests, and migrate in small slices, verifying each step as you go.

••

Want to apply this in your company?

Book a call with Juan →

Legacy Code With AI: How to Document and Migrate Safely

To use AI on legacy code safely, follow four steps: map the system, document what it actually does, write characterization tests that freeze its current behavior, and migrate in small slices. AI speeds up reading and first drafts, but every output must pass an executable check, such as a test or a comparison of results, before it becomes a decision.

Key takeaways

  • AI reads and summarizes code quickly, but it does not know what the business expects. Your team validates behavior, using tests and the people who use the system.
  • Document first, migrate second. A reviewed document works as the contract for the migration.
  • Characterization tests record what the system does today, including what looks like a bug, and protect every step of the change.
  • Migrate in slices with a clear way back: one slice, one acceptance criterion, one rollback plan.
  • Limit what the AI can see and what it can run. Legacy code often contains secrets, personal data and rules nobody remembers the reason for.

Why does legacy code need extra care with AI?

In a new project, the code is usually small, recent and covered by tests. In legacy systems the opposite is true: knowledge is scattered across the code, the database, spreadsheets, scheduled jobs and the memory of a few people. A language model can read the file, but it cannot read context that is not in it.

This creates a specific risk. AI produces fluent, plausible explanations even when it is wrong. In old code, a plausible but wrong explanation is worse than none, because it becomes documentation, then a migration requirement, and only shows up as a defect in production.

That is why the plan below treats the AI as an assistant that proposes and the team as the party that confirms. Each step ends with a check that does not depend on the model's opinion: a test that passes, a query that matches, a business person who recognizes the rule.

Step 1: How do you map the system before asking the AI anything?

Mapping means knowing what exists, what depends on what, and what is critical. Without it, the AI receives loose pieces and returns loose answers. The same principle applies to any automation, as we describe in mapping business processes before applying AI: process and baseline first, tool second.

A useful map of a legacy system answers these questions:

  • What are the modules, services and scheduled jobs, and who is responsible for each one?
  • Which external integrations exist (payments, invoicing, ERP, WhatsApp, email) and how do they fail?
  • Where is the data, which tables are central, and which fields hold personal data?
  • Which parts change often, and which has nobody touched in a long time?
  • If something stops, what interrupts operations the same day?

AI helps here in a modest, verifiable way. It can list imported dependencies, generate an inventory of entry points (routes, commands, jobs), summarize the folder structure and point to code that calls external systems. Every item in the inventory should be checked by searching the code or running it, not accepted on trust.

The output of this step is a short inventory and a risk rating per module. It also sets the order of work: start with parts important enough to justify the effort, but isolated enough to fail without taking everything down.

Step 2: How do you document with AI without recording fiction?

Documenting with AI works well when the request is narrow and the evidence is cited. Asking the model to explain the whole system produces generic text. Asking it to explain one specific function, pointing to the lines that support each claim, produces something you can check.

A simple method, module by module:

  • Give the AI only the module and its direct dependencies, not the whole repository. Lean, relevant context tends to produce better answers, as Anthropic discusses in its material on context engineering for agents.
  • Ask for three separate things: what the code does, the business rules it implements, and the points where the AI is unsure.
  • Require a file and line reference for every claim. A claim without a reference is discarded.
  • Label each part of the document as confirmed, hypothesis or unknown.
  • Take the hypotheses to the people who run the process. Ask a concrete question: when an order has this status, the system does this. Is that what you expect?

The split between confirmed, hypothesis and unknown is the most important part. It prevents a model's guess from being read, months later, as an official rule. It also shows where the company depends on knowledge that exists only in someone's head, which is already a valuable finding.

If the team uses an agentic coding tool, it is worth recording the repository's conventions and verification commands in an instructions file. We explain how in how to write a good CLAUDE.md. The generated documentation should live in the repository itself, versioned, so it is reviewed like any other change.

Step 3: What are characterization tests, and how does AI help?

A characterization test records what the code does today, without judging whether it is right. You call the function with varied inputs, note the real outputs and turn them into a test. If a future change alters an output, the test fails and you decide whether the difference is a wanted improvement or a break.

This is the safety net for the migration. Without it, any rewrite depends on faith. With it, the question becomes objective: does the new code behave like the old one in the cases that matter?

AI is useful for generating input variations and drafting the tests, especially for edge cases the team would forget: empty fields, boundary dates, negative values, accented characters, rounding. The caution is this: the generated test must be run against the current code, and the expected output must come from the real execution, never from the model's assumption. If the model writes the expectation from memory, you are testing its imagination, not the system.

A practical walkthrough:

  • Choose the slice to migrate and list its observable inputs and outputs (function returns, records written, messages sent, files generated).
  • Ask the AI for a set of varied inputs, including edge cases and anonymized real cases.
  • Run those inputs on the current system and record the outputs as the expected result.
  • Review the strange-looking results with someone who knows the business. Some are old defects, others are hidden rules.
  • Decide case by case whether the defect will be preserved in the migration or fixed explicitly, with a record.

To judge whether the test set is trustworthy, the idea of continuous evaluation described in Anthropic's and OpenAI's guides on evals applies well: fixed, repeatable cases with a verifiable result. When the use of AI is itself part of the workflow, the article on how to measure an AI automation pilot helps you define metrics and decision criteria.

Step 4: How do you migrate in parts without stopping operations?

Migrating everything at once is the most expensive way to find out what you did not know. The alternative is to cut the system into slices and replace one at a time, keeping the old and new versions running side by side until confidence is high enough.

The routine for each slice:

  • Define the slice boundary: what goes in, what comes out, who calls it.
  • Make sure the slice's characterization tests pass on the old code.
  • Ask the AI for a draft of the new version, in small pieces, using the step 2 document and the tests as reference.
  • Run the same tests on the new code. Every difference is investigated, not patched in a hurry.
  • Run the new code in parallel or shadow mode, comparing results with the old one without affecting users.
  • Release gradually, with a switch to return to the old code.
  • Turn off the old code only after an agreed period without relevant divergences.

The AI acts as a drafter and reviewer, not the final author. Ask it to explain what changed between the old and new versions and to list risks. Then confirm the risks with the tests. If the model says the behavior is equivalent, the right response is to run the comparison, not to accept the sentence.

Also keep migration separate from improvement. Rewriting in a new language and changing business rules in the same step mixes causes and makes any difference impossible to diagnose. Preserve behavior first, then evolve it.

What does this look like in practice? An illustrative scenario

Illustrative scenario: a distributor has an old module that calculates shipping cost and delivery time. It was written many years ago, has no tests, and only one person in logistics can explain some of its exceptions. The company wants to move it to a new service.

In the mapping step, the team finds that the module is called by the online order flow, by WhatsApp customer support and by a quoting spreadsheet. In documentation, the AI describes the main rule and flags a doubt: why do certain postal codes have a fixed surcharge? The logistics person explains it is a region served by a specific carrier. The rule becomes confirmed text.

In characterization testing, the AI generates varied inputs and the team runs them all on the old module, recording the outputs. One result stands out: orders with a weight exactly at a bracket limit pay the higher bracket. Logistics confirms it is intentional. The test stays, with a comment explaining why.

In migration, the first slice is only the delivery time calculation. The new service runs in shadow mode, comparing its answers with the old one. Divergences are investigated one by one. Only then is the slice released, with a switch to go back, and shipping cost is left for the next round.

No step depended on believing the AI. Each one ended in a test, a comparison or a confirmation from someone who knows the process.

What limits and controls should you set before starting?

Legacy code usually carries three security problems: secrets embedded in the code, personal data in sample files or logs, and old dependencies with known flaws. Before pointing an AI tool at the repository, define what it can read, what it can run and where the data goes.

  • Remove or mask secrets before sharing any snippet with a model, and replace any that were ever exposed.
  • Never use real customer data in prompts or tests. Anonymize it or generate synthetic data. LGPD applied to AI projects requires attention to purpose, minimization and to whom the model vendor counts as a processor.
  • Give the coding tool minimal permissions: read-only by default, execution in an isolated environment, no access to the production database. See Claude Code security: permissions, sandbox and secrets.
  • Treat repository content as untrusted input. Comments and files can contain instructions that try to divert the agent, a risk cataloged by OWASP in its material on LLM applications.
  • Require human review of all code the AI proposes, with the tests running before approval.

The method we apply in AI projects follows the same logic: map the current process and baseline, limit data and actions, pilot one real slice with records and an agreed success criterion, and hand over with an explicit decision to expand, adjust or stop. In legacy code, the pilot slice is a small but relevant module, and the decision to continue rests on the tests and the divergences observed, not on an impression that the AI helped.

Frequently asked questions

Can AI migrate an entire legacy system on its own? It is not recommended. AI is good at drafting pieces, explaining code and generating test cases, but it does not know rules that exist only outside the code. A safe migration depends on characterization tests, human review and gradual release.

Where do you start documenting a system with no documentation? Start with the most critical module that is reasonably isolated. Ask the AI for a description with file and line references, label each claim as confirmed, hypothesis or unknown, and validate the hypotheses with the people who run the process. Store the result in the repository.

Is it safe to send legacy code to an AI tool? It depends on what the code contains and on your contract with the vendor. Remove secrets and personal data, define which repositories may be shared, and confirm the data processing terms. If you are unsure about personal data, consult guidance from Brazil's data protection authority (ANPD) and your company's legal team.

How do you know whether the migration changed the system's behavior? Compare the new code with the old using the same characterization tests and, when possible, run both in parallel on anonymized real data. Every divergence must be classified as an intentional improvement or a break, and recorded. Without that comparison, equivalence is just an opinion.

Sources

legacy codeAI documentationcode migrationcharacterization testssoftware modernization