---
title: "Refactoring and migrations at scale | Optimize All Academy"
description: "Why agents change the economics of large changes Refactors and migrations used to be postponed because they were tedious: hundreds of mechanical edits…"
url: https://optimizeall.com/learn/agentic-coding-with-ai/refactoring-and-migrations-at-scale
updated: 2026-10-05
---

AI-Assisted Software Development: Coding Agents in Practice · Code review, migrations and CI integration · lesson 11 of 17 · 14 min

# Refactoring and migrations at scale

## Why agents change the economics of large changes

Refactors and migrations used to be postponed because they were tedious: hundreds of mechanical edits, each slightly different. Agents are good at exactly that kind of work, *if* you give them structure. Unstructured, a large migration produces an inconsistent, unreviewable mega-diff.

## Choose the right tool for each part

Not every change needs an LLM:

| Change type | Best tool |
|---|---|
| Purely syntactic, uniform (rename, import path change) | IDE refactoring, `sed`, codemods (jscodeshift, ts-morph), OpenRewrite for Java, LibCST for Python |
| Patterned but varied (API migration with several call shapes) | Agent guided by a skill with before/after examples, or agent that writes a codemod |
| Semantic (new architecture, behavior changes) | Agent in plan-implement-verify loop, human-designed plan |

A powerful hybrid: **ask the agent to write the codemod**, test it on a sample, then run it deterministically across the codebase. Deterministic transforms are reproducible and reviewable as code.

```text
Write a jscodeshift transform that replaces `logger.log(level, msg, meta)` calls with
`log[level](msg, meta)` and adds `import { log } from "@acme/obs"` where needed.
Include tests for: plain calls, calls with template literals, calls inside arrow functions,
and files that already import log. Run it on src/billing/ only and show the diff summary.
```

## The migration playbook

1. **Inventory.** Use search or a subagent to list every usage and group by pattern. Save as `MIGRATION.md`.
2. **Characterize.** Before changing anything, make sure tests cover current behavior of affected code. Add characterization tests (tests that pin current behavior, even if odd) where coverage is thin.
3. **Write the recipe.** For each pattern: before/after example, edge cases, and what to do if unsure (leave a `TODO(migration)` and list it).
4. **Pilot.** Migrate one small, well-tested module. Review carefully. Refine the recipe.
5. **Batch.** Migrate in slices (per package or directory), each a separate PR with tests green. Parallel agents in worktrees can work on different slices.
6. **Track.** Update the inventory with done, in progress, and exceptions.
7. **Remove the old path.** Delete the old library or code only when the inventory is empty; add a lint rule to prevent reintroduction.

## Keeping the system working throughout

Large changes are safest when the system is releasable after every slice:

- **Adapters and shims.** Introduce the new interface, adapt the old one to it, and migrate callers gradually.
- **Feature flags** for behavior changes, so you can roll back without a revert.
- **Expand and contract** for database changes: add new columns, dual-write, backfill, switch reads, remove old columns.

## Worked example: framework upgrade in a regional e-commerce app

A team in Dubai upgraded a React app across a major version with breaking changes to routing and data fetching. Their approach:

- An agent read the official migration guide and the codebase and produced `MIGRATION.md` with 11 patterns and 340 call sites.
- Mechanical patterns (6 of 11) were handled by codemods the agent wrote and tested.
- Semantic patterns (data fetching) were migrated by agents route by route, each PR with before/after screenshots from an automated browser check.
- The lint rule banning the old import landed with the final PR.

The critical choice was forcing the agent to consult the *official migration guide* for the exact target version rather than its memory, which blended advice from several versions.

## Hands-on: MIGRATION.md template

```markdown
# Migration: <old> → <new>   (owner: @name, started: YYYY-MM-DD)
## Official references
- <link to the vendor's migration guide for the exact target version>
## Patterns
| # | Pattern | Before | After | Tool (codemod/agent/manual) | Count |
## Inventory
| Path | Pattern # | Status (todo/in-progress/done/exception) | PR |
## Exceptions and decisions
## Done when
- Inventory empty; old dependency removed; lint rule prevents reintroduction; all tests green.
```

## Pitfalls

- **One giant PR.** Unreviewable and impossible to bisect.
- **No characterization tests.** You cannot prove behavior is unchanged.
- **Version confusion.** Agents trained on many versions mix APIs; always pin the target version's docs in context.
- **Silent exceptions.** The 5% of odd cases are where incidents come from; list them explicitly.

## How to measure success

Track inventory burn-down, PRs per slice merged without rework, and incidents attributable to the migration (target: zero).

## Video lecture: Refactoring and migrations at scale

Lecture coming soon · 15 chapters · about 9 minutes. Read the full transcript below.

1. Refactors and migrations at scale
2. Analogy: moving house
3. Right tool
4. Hybrid: agent writes the codemod
5. Playbook 1–4
6. Playbook 5–7
7. Always releasable
8. Case: Dubai framework upgrade
9. Pitfalls
10. Measuring a migration
11. Example: replace a date formatter
12. Common mistakes
13. Deeper: renaming a column safely
14. Watch me do it: MIGRATION.md
15. Recap

## Lecture transcript

### Refactors and migrations at scale

Every codebase has a migration nobody wants to do: the deprecated library, the framework upgrade, the logging rewrite. Agents change the economics of that work, but only if you bring structure. In this lecture you will learn which tool fits which kind of change, a seven-step migration playbook, and techniques that keep your system releasable the whole way through.

### Analogy: moving house

Here is an analogy. A large migration is like moving house with a family. If everyone throws everything into random boxes on the same day, you spend months searching for the kettle. Good movers label every box by room, move one room at a time, keep the kitchen working until the new one is ready, and only hand back the old keys when the last box is gone. The migration playbook in this lecture is that moving plan: inventory, label, move room by room, keep things working, then hand back the keys.

### Right tool

First, not every change needs a language model. Purely mechanical, uniform changes, like a rename or an import path, are best done with your IDE's refactoring tools or a codemod, which is a program that transforms code. Patterned but varied changes, like an API migration with several call shapes, suit an agent guided by clear before and after examples. Truly semantic changes, like a new architecture, need the full plan, implement, verify loop with a human-designed plan.

### Hybrid: agent writes the codemod

Here is a powerful hybrid. Ask the agent to write the codemod. It writes the transform, plus tests for the tricky shapes, and runs it on one folder first. Then you run it deterministically across the codebase. Deterministic transforms are reproducible, reviewable as code, and consistent in a way that a thousand separate model edits never are.

### Playbook 1–4

Now the playbook. One: inventory every usage and group by pattern, saved in a MIGRATION dot M D file. Two: characterize, meaning add tests that pin current behavior where coverage is thin. Three: write the recipe for each pattern, with examples and edge cases. Four: pilot on one small, well-tested module and refine the recipe.

### Playbook 5–7

Five: migrate in batches, one package or directory per pull request, tests green every time. Parallel agents in separate worktrees can take different slices. Six: track status in the inventory: done, in progress, exceptions. Seven: remove the old path only when the inventory is empty, and add a lint rule so nobody reintroduces it.

### Always releasable

Throughout, keep the system releasable. Introduce the new interface with an adapter, so old and new can coexist. Use feature flags for behavior changes, so you can roll back without a revert. And for databases, use expand and contract: add new columns, write to both, backfill, switch reads, and only then remove the old columns.

### Case: Dubai framework upgrade

A real-world shaped example from Dubai. A team upgraded a React app across a major version with breaking changes. An agent read the official migration guide and the codebase, and produced an inventory of eleven patterns across hundreds of call sites. Six mechanical patterns were handled by codemods the agent wrote and tested. The data-fetching patterns were migrated route by route, each pull request with automated before and after screenshots. The final pull request added a lint rule banning the old import.

### Pitfalls

The single most important choice in that project was forcing the agent to read the official migration guide for the exact target version, instead of relying on memory. Models have seen many versions of popular frameworks, and they blend them. Other pitfalls: one giant pull request that no one can review or bisect, missing characterization tests, and silent exceptions. The odd five percent of cases is exactly where incidents come from, so list them explicitly.

### Measuring a migration

How do you know the migration is going well? Watch three numbers. The inventory burn-down, which should drop steadily slice by slice. The share of slice pull requests merged without rework, which tells you whether your recipe is good; if it falls, stop and fix the recipe before continuing. And incidents attributable to the migration, where the target is zero. If an incident does happen, add the case to the recipe and the tests, so the remaining slices benefit immediately.

### Example: replace a date formatter

A simple example. You need to replace a deprecated date library's format function across a hundred and twenty files. There are two call shapes: format with a pattern string, and format with a locale. You ask the agent to write a codemod handling both, with tests for each shape plus one file that already uses the new library. It runs the codemod on one folder, you review twelve diffs, spot one edge case with a template string, the agent fixes the codemod, and then it runs across the whole codebase in seconds, identically everywhere.

### Common mistakes

Common mistakes, and they are expensive ones. Starting with the biggest, most tangled module instead of a small, well-tested pilot. Skipping characterization tests, so you cannot prove behavior is unchanged. Letting agents improvise on patterns instead of following a written recipe, which produces three different styles of the same change. And declaring victory before the old library is actually removed, leaving two ways to do everything for years. Ask yourself: which migration in your codebase has been ninety percent done for more than six months?

### Deeper: renaming a column safely

Let's deepen the expand-and-contract example with a concrete column rename, customer name to full name. Release one: add the full name column. Release two: write to both columns. Then backfill old rows in batches overnight. Release three: read from full name. Release four, a week later: drop customer name. At every step the app works and you can roll back.

### Watch me do it: MIGRATION.md

Watch me do it. I'll fill in MIGRATION dot M D for replacing a deprecated logger across a TypeScript monorepo. First, the official reference: I paste the link to the new library's migration guide for the exact version we are adopting, and tell the agent to use it instead of memory. Second, the inventory: a subagent searches every package and groups usages into three patterns. Pattern one, plain calls, two hundred and ten sites. Pattern two, child loggers with context, sixty-one sites. Pattern three, custom formatters, eight sites. Third, the recipe: for each pattern, a before and after example and the tool. Pattern one: codemod. Pattern two: agent guided by a skill. Pattern three: manual. Fourth, the pilot. I pick the billing package because it has strong tests. The agent writes the codemod with tests for the tricky shapes, runs it on billing only, and I review the diff. One edge case appears: calls with template literals lose their interpolation. The agent fixes the codemod and adds a test. Fifth, batches: one pull request per package, each updating the inventory status column. Finally, the done-when section: inventory empty, old dependency removed from the lock file, a lint rule banning the old import, all tests green. That file is now the single source of truth for the whole migration.

### Recap

Recap. Pick the right tool per change type, and let agents write codemods for mechanical work. Follow the playbook: inventory, characterize, recipe, pilot, batch, track, remove. Keep the system releasable with adapters, flags and expand and contract. Your next step: copy the MIGRATION dot M D template from the lesson and start an inventory for the migration your team keeps postponing.

## Key takeaways

- Match the tool to the change: codemods for mechanical, agents with examples for patterned, full loop for semantic.
- Have agents write and test codemods, then run them deterministically.
- Follow the playbook: inventory, characterize, recipe, pilot, batch, track, remove.
- Keep the system releasable with adapters, feature flags and expand-and-contract; pin the target version's docs.

## Try it

Create a MIGRATION.md for a postponed migration in your codebase, with an inventory of patterns and a pilot module.

- [Previous: Code review with AI, and reviewing AI code](https://optimizeall.com/learn/agentic-coding-with-ai/ai-code-review)
- [Next: Integrating agents into CI with GitHub Actions](https://optimizeall.com/learn/agentic-coding-with-ai/ci-integration-github-actions)
- [All lessons of AI-Assisted Software Development: Coding Agents in Practice](https://optimizeall.com/learn/agentic-coding-with-ai)
