How I use Claude Code to refactor legacy code — without breaking production
Legacy code refactoring is one of the most dangerous operations in software engineering. One wrong move and you're rolling back a production deploy at 2am.
After dozens of refactoring sessions with Claude Code, I've developed a workflow that lets me modernize old codebases safely — and without hitting rate limits mid-session.
The problem with legacy code
Legacy codebases have three properties that make refactoring hard:
- No tests — or tests that are so brittle they break when you change a variable name
- Hidden dependencies — that function you're refactoring is called from 47 places you didn't know about
- Tribal knowledge — the original author left two years ago
Claude Code handles all three — but only if you structure the session correctly.
Phase 1: Understand before touching anything
Before writing a single line of refactored code, I spend the first 15 minutes of every session mapping the existing code.
# Start Claude Code with a read-only mandate
claude
Then I give it this prompt:
Do NOT modify any files yet. I need you to understand this codebase first.
1. Read all files in src/
2. Find every place [legacy function/module] is called
3. Identify what inputs it receives and what outputs it produces
4. List all external dependencies (database, APIs, other services)
5. Write a summary in a file called REFACTOR_PLAN.md
Do not touch any source files until I explicitly tell you to proceed.
This produces a REFACTOR_PLAN.md that becomes your source of truth for the entire session.
Phase 2: Write tests BEFORE refactoring
This is the step most developers skip. Claude Code makes it easy to do right.
Now read REFACTOR_PLAN.md. Based on the current behavior you documented:
1. Write integration tests for [legacy module] that test the CURRENT behavior
2. These tests should pass against the EXISTING code
3. Put them in tests/legacy-[module].test.js
4. Run them and confirm they pass
Do not refactor anything yet.
If Claude Code gets this right, you now have a safety net. The refactored code must pass these same tests.
Phase 3: Refactor in small increments
This is where most sessions blow up — trying to refactor everything at once.
Instead, I use this incremental approach:
I want to refactor [specific function/module] only. Leave everything else untouched.
Requirements:
- The existing tests in tests/legacy-[module].test.js must still pass
- Keep the same function signature (inputs and outputs must be identical)
- The refactored version should use [modern pattern: async/await, TypeScript, etc.]
- After each change, run the tests
Start with the smallest possible change.
The key constraint: same function signature. This lets you swap in the new implementation without touching any callers.
Phase 4: The strangler fig pattern
For large modules, I use the strangler fig pattern — route traffic to the new implementation gradually.
I have:
- legacy/payment-processor.js (old code)
- src/payment-processor.js (new refactored code)
Create a feature flag wrapper:
- If process.env.USE_NEW_PAYMENT_PROCESSOR === 'true', use src/
- Otherwise fall back to legacy/
- Log which version handled each request
- Add monitoring for any errors in the new version
This lets you test the new implementation in production with real traffic, with instant rollback capability.
The rate limit problem
Here's what nobody tells you about refactoring with Claude Code: it burns tokens fast.
A typical legacy refactoring session involves:
- Reading 20-50 source files to understand dependencies
- Writing tests (multiple rounds of fix → run → fix)
- Writing the refactored code
- Running tests again
- Debugging failures
- Documentation
I regularly hit Claude's rate limits mid-session. Right at the moment when I've built up full context about the codebase, the session dies.
My solution: I use SimplyLouie as my API endpoint. It's a Claude API proxy at ✌️2/month — I configure it once:
export ANTHROPIC_BASE_URL=https://simplylouie.com
export ANTHROPIC_API_KEY=my-simplylouie-key
claude
And my sessions never get interrupted mid-refactor.
REFACTOR_PLAN.md template
Here's the template I use for every legacy refactoring project:
# Refactoring Plan: [Module Name]
## Current behavior
- Input: [what it receives]
- Output: [what it returns]
- Side effects: [what it modifies]
## Dependencies
- Called by: [list of callers]
- Calls: [list of dependencies]
- External services: [databases, APIs, queues]
## Known issues
- [Bug 1]
- [Performance issue 1]
## Refactoring approach
- Strategy: [strangler fig / in-place / extract module]
- Feature flag: [env var name]
- Rollback plan: [how to revert]
## Test coverage
- [ ] Happy path
- [ ] Error cases
- [ ] Edge cases
- [ ] Performance baseline
## Migration steps
1. [ ] Write tests against current behavior
2. [ ] Implement new version
3. [ ] Route 1% traffic to new version
4. [ ] Route 10% traffic
5. [ ] Route 100% traffic
6. [ ] Remove legacy code
The mindset shift
The biggest mistake I made early on was treating Claude Code as a "write the refactored code" tool.
It's actually better as a "understand the existing code" tool first.
Once Claude Code deeply understands what the legacy code does, the refactored version almost writes itself. The understanding phase is where sessions get long, where tokens get consumed, where rate limits hit.
Plan for that. Structure your sessions around the understanding phase. And have a reliable API endpoint for when the rate limit hits at the worst possible moment.
I use SimplyLouie as my Claude Code API endpoint — ✌️2/month, no rate limit interruptions during critical refactoring sessions. 7-day free trial, no commitment.
Top comments (0)