AI coding
Key points

Key points

  1. 1Legacy code is hard for agents for the same reasons it is hard for people: no tests, missing documentation, unwritten conventions, old dependencies and slow feedback.
  2. 2Start by using the agent to understand the system, from overview down to individual functions, and have a person check what it writes before anyone changes code.
  3. 3Pin down current behavior with characterization tests before changing anything, then make small, safe refactorings at seams until the code is testable.
  4. 4Replace large pieces gradually behind a facade, using the strangler fig pattern, and give the agent a context file that records the system's versions, conventions and test commands.

Why legacy code is hard for agents

Michael Feathers defined legacy code in a way that has outlived the book it came from: "Legacy Code is code without tests" (Understand Legacy Code). Erik Dietrich, discussing the same book, offers a second definition that fits just as well: "Legacy code is any code that you don't want to touch because it's fragile" (DaedTech).

Coding agents are at their best on fresh, well-tested code, and at their weakest on exactly this kind. Driver's co-founder calls it the migration paradox: migrations are "the highest-leverage use case for AI coding agents," and also "where agents need the most context and have the least." The legacy system "is the thing nobody fully understands. That's why you're migrating in the first place" (Driver).

A group of engineers who compared notes on using AI tools in older codebases named four recurring problems (The Friday Deploy):

  • Missing conventions. The tool misses the library of business-specific helpers or the shared component library "and just reinvented everything for you."
  • Unsafe changes. Database migrations and API changes in a production system must stay backward compatible, which "requires far more careful review of generated code."
  • Old dependencies. Tools may assume functions from newer library versions "that you don't yet have access to."
  • Long feedback loops. Slow builds, tests that need many services, or features that can only be tested in staging leave the agent unsure which checks to run.

None of these are reasons to keep agents away from legacy code. They are a list of things to set up before the agent starts editing.

Use the agent to understand before it changes anything

The safest first use of an agent on a legacy system changes nothing at all. It reads and explains.

The engineers in The Friday Deploy's discussion found that AI "can do a great job of explaining code," and that by having it "methodically describe your codebase a chunk at a time, you can build up very complete documentation" (The Friday Deploy). They also found it effective during incidents: given a specific error message, it could trace the code path and explain how the system got into that state.

Order matters. Researchers studying how professional code auditors learn unfamiliar codebases found that their understanding "flows from global to local levels": first the project overview, then the codebase structure and business logic, then individual functions and variables (arXiv). The same paper notes earlier findings that LLM tools on their own often fail to supply the business logic needed to understand a codebase. Follow the auditors' order and fill that gap with people.

A practical sequence:

  1. System map. Ask the agent to list entry points, major modules, external systems, data stores and scheduled jobs, with file paths for each. Save it as a document in the repository.
  2. Business flows. For each important flow, such as "a customer places an order", ask for the path through the code, step by step, with file and function names.
  3. Function summaries. For the code you will change, ask for plain-language summaries of what each function does, its inputs, outputs and side effects. Forte Group describes doing this at large scale, generating reviewable summaries for thousands of functions and indexing them so new engineers and product owners could explore the system (Forte Group).
  4. Human review. Have someone who knows the system correct the documents. Mark anything uncertain as uncertain.

The documents are useful twice: they help people now, and they become context for every later agent session. Driver's article makes the point that this understanding work is worth paying for before committing to a migration: one consultancy chose to spend a few weeks understanding a large codebase "before we invest in it and put our reputation on the line."

Pin current behavior with characterization tests

Feathers' recipe for changing legacy code starts with tests: "First, add tests, then do your changes" (Understand Legacy Code). The difficulty is that legacy code often has no tests, and the existing behavior may not match any written requirement.

Characterization tests solve that. Dietrich explains the idea: if a method called AddTwoNumbers returns 12 when given 1 and 1, "you don't say 'that's wrong.' Instead you write a test that documents that return value and you move on" (DaedTech). The reason is practical. "When code goes live, people work their processes around it, however much its behavior may be goofy or unintended." The test records what the system does, so you can see when a change makes it behave differently.

The technique suits agents well, with two guardrails:

  • Write the tests in a separate step, and commit them before any refactoring. The agent that will change the code should not be the only author of the tests that judge the change.
  • Treat the tests as read-only during refactoring. If a test looks wrong, the agent stops and reports it rather than editing it.

A prompt for the first step:

Write characterization tests for calculate_invoice_total in billing/invoice.py.
Record what the function returns today for each case below, even if the
result looks wrong. Do not change any production code.
Cases: empty order, one item, discount code, expired discount, tax-exempt
customer, currency other than USD, negative quantity.
Run the tests and show they pass against the current code.

Odd results are information. A characterization test that captures a surprising value is either documenting a real business rule or a real bug. Either way, a person should decide which before anyone "fixes" it.

Break dependencies in small, safe steps

Getting tests in place usually requires changing the code, which is what you were trying not to do without tests. Feathers calls this the legacy code dilemma, and his answer is to "change as little code as possible to get tests in place" (Understand Legacy Code). The recipe:

  1. Identify change points.
  2. Break dependencies.
  3. Write the tests.
  4. Make your changes.
  5. Refactor.

The hard part is the dependencies: code that cannot run in a test because it opens a database connection, calls a third-party service or needs a complex object. Feathers' tool is the seam, "a place to alter program behavior, without changing the code," such as passing in an object that a test can replace with a fake.

This is mechanical, careful work that agents do well when the steps are small. Give one refactoring per task, name it, and require the characterization tests to pass before and after:

In billing/invoice.py, calculate_invoice_total creates its own TaxService.
Change it to accept an optional tax_service parameter that defaults to
TaxService(), so tests can pass a fake. Make no other changes.
Run tests/characterization/test_invoice.py before and after; both runs must pass.

Avoid large refactors in one step. Each small, behavior-preserving change is easy to review and easy to revert. A large one, even a correct one, makes the code unrecognizable to the people who maintain it.

Replace large pieces gradually

Sometimes a module or a whole system needs replacing, not just refactoring. The safest approach is gradual. Martin Fowler named it the strangler fig, after vines that grow around a host tree until they can stand on their own, and the term "is now often used to describe a gradual approach to legacy modernization" (Martin Fowler).

Microsoft's architecture guidance describes how it works in practice (Microsoft Learn):

  1. Put a facade, such as a proxy, between clients and the legacy system. At first it routes nearly everything to the old code.
  2. Rebuild one piece of functionality in the new system.
  3. Shift that piece's traffic through the facade to the new system.
  4. Repeat until the legacy system has nothing left to do, then retire it.

The pattern "allows the existing application to continue functioning during the modernization effort" and lets teams move "at a pace that suits the complexity of the project." Microsoft's guidance also describes keeping rollback possible during data migration, so each step can be undone.

Agents fit naturally into each slice. They can map the behavior of the piece being replaced, write characterization tests against the old implementation, build the new implementation against those same tests, and produce the routing change. People decide the order of slices, approve each cutover and watch production.

Write the system's rules down for the agent

Every problem The Friday Deploy's engineers listed can be eased with a project context file. One of their tactics was exactly this: rules files that describe "conventions to obey when working in your codebase" and the test steps for unusual tools (The Friday Deploy).

For a legacy system, the file should cover what the agent cannot guess:

## Versions (do not upgrade without a task that says so)
- Python 3.8, Django 2.2. Do not use syntax or APIs from newer versions.

## Use existing helpers
- Money: billing/money.py. Dates: core/dates.py. HTTP calls: core/http_client.py.
- Search for an existing helper before writing a new one.

## Tests
- Fast checks: `make test-unit` (no services needed).
- Billing changes also need: `make test-billing` (starts Postgres in Docker).
- Characterization tests in tests/characterization/ are read-only.

## Never
- Change a database column type or drop a column.
- Change the response shape of any /api/v1 endpoint.

Each line answers one of the known failure modes: old versions, reinvented helpers, unclear test steps, unsafe changes. Add to the file whenever the agent makes a mistake that the file could have prevented.

The engineers made a related point: "Best practices for people often work for AI tools." Shorter functions, clear module boundaries and short readme files per area help the next agent session as much as the next new hire. Asking the agent to write some of that documentation, and checking it, improves the codebase for both.

Where agents help most, and where people decide

WorkAgent's rolePerson's role
Mapping the system and business flowsDrafts maps and summariesCorrects them and marks unknowns
Characterization testsWrites and runs themDecides whether odd results are rules or bugs
Breaking dependenciesMakes one small refactoring at a timeReviews each and keeps the tests read-only
Finding duplicate logicSpots equivalent code across filesChooses what to consolidate
Strangler fig slicesRebuilds a slice against the same testsOrders slices and approves each cutover
IncidentsTraces an error message through the codeDecides the fix and the rollout

Forte Group's account of modernization work gives a sense of where the leverage is: finding functionally equivalent code across a large application, dissecting oversized functions into smaller units, and proposing tests to prove the rewritten code behaves the same (Forte Group). These are exactly the tasks that are tedious for people and well suited to an agent working under tests.

The constant across all of it is Feathers' principle. Get the behavior under test first, change in small steps, and keep the system working the whole way. Agents make each step faster. They do not make it safe to skip steps.

FAQ

FAQ

Should I let an agent rewrite a legacy module from scratch?

Rarely, and never without tests that capture its current behavior first. A rewrite discards years of fixes for cases nobody wrote down. Replace it gradually behind a facade, one slice at a time, with characterization tests proving each slice behaves the same.

What if the legacy code has no tests at all?

Start with characterization tests that record what the code does today, including behavior that looks wrong. Make only the smallest changes needed to get code under test, such as adding a parameter so a dependency can be replaced. Then refactor with those tests as the safety net.

How do I stop the agent from using newer library features?

State the exact versions in the project context file and say they must not be upgraded without a task that says so. Make sure the build and tests run against those versions, so any use of a newer feature fails quickly. Point the agent to the existing helpers it should use.

Can the agent document a system nobody understands anymore?

It can produce a strong first draft by reading the code from the top down, starting with entry points and major modules. The draft will miss business reasons that live only in people's heads. Have someone who knows the domain review it and mark anything uncertain.

Is the strangler fig pattern only for whole systems?

No. The same idea works for a single module or endpoint: route calls through one interface, build the replacement behind it, switch traffic over, and remove the old code. Keep the ability to switch back until the new path has proven itself.

Sources

Sources

  1. https://understandlegacycode.com/blog/key-points-of-working-effectively-with-legacy-code/
  2. https://daedtech.com/characterization-tests/
  3. https://www.driver.ai/blog/ai-coding-agents-should-make-migrations-easier-without-context-they-make-them-worse
  4. https://thefridaydeploy.substack.com/p/ai-cant-handle-your-legacy-codebase
  5. https://arxiv.org/html/2504.04553v2
  6. https://fortegrp.com/insights/modernizing-legacy-codebases-with-llms
  7. https://martinfowler.com/bliki/StranglerFigApplication.html
  8. https://learn.microsoft.com/en-us/azure/architecture/patterns/strangler-fig
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles