How I use Claude Code to refactor legacy code — a complete workflow
Every developer has that one codebase. The one that's been around for 8 years. The one with no tests, spaghetti dependencies, and functions that do 12 things at once. The one everyone's afraid to touch.
This is my workflow for refactoring legacy code with Claude Code — without breaking production.
Why legacy refactoring is hard
The problem isn't the code itself. It's the unknown blast radius. You don't know what will break when you touch something. Classic refactoring books say "write tests first" — but what if there are no tests and writing them would take weeks?
Claude Code sessions for legacy work run long. Really long. We're talking 3-4 hour sessions with hundreds of files in context. That's where rate limits bite you — right in the middle of understanding the worst parts of the codebase.
My workflow
Phase 1: Reconnaissance (don't touch anything yet)
Start with a pure analysis prompt. No changes, just understanding:
Analyze this codebase for refactoring opportunities. Do NOT suggest any code changes yet.
For each file, identify:
1. What is the single responsibility of this module?
2. What are its hidden dependencies (globals, side effects, implicit state)?
3. What would break if we changed this module's interface?
4. What test would tell us if we broke it?
Start with the files that have the most incoming dependencies.
This gives you a dependency map before you touch anything.
Phase 2: Write characterization tests
Before refactoring, lock down existing behavior — even if it's wrong:
For [specific_module.js], write characterization tests that capture its CURRENT behavior.
These are not aspirational tests. They document what the code does NOW, including:
- Edge cases that seem like bugs but might be relied upon
- Implicit assumptions about input format
- Side effects on shared state
- Error handling behavior
Use Jest. Make the tests pass against the current code before we change anything.
Characterization tests are your safety net. They don't say the code is correct — they say "this is what currently happens."
Phase 3: Incremental extraction
Now you can start moving safely:
Refactor [function_name] in [file.js] using the strangler fig pattern:
1. Create a new function with the same signature in a new file
2. Have the old code delegate to the new function
3. Move one behavior at a time, running tests after each move
4. The old function should be a thin wrapper by the end
Do NOT change the external API. Do NOT change how callers use this.
Show me the diff after each step.
The strangler fig pattern means you never have a broken intermediate state. Old callers keep working while you migrate underneath them.
Phase 4: Dependency untangling
Legacy code loves globals and hidden state. Here's how to surface and remove them:
This module uses these implicit globals: [list them]
Refactor it to make all dependencies explicit via constructor injection:
1. Identify every place a global is read or written
2. Add those as constructor parameters with sensible defaults
3. Update the existing call sites to pass the globals explicitly
4. Mark the constructor parameters with JSDoc so future devs know what's happening
Keep backward compatibility — don't break existing callers.
Phase 5: Extract and name
Now that dependencies are explicit, naming becomes possible:
This function is 200 lines long and does too many things.
Identify the distinct responsibilities and suggest extraction points where:
- The extracted function has a clear, single name
- No new parameters are needed (state is already passed in)
- Each extracted piece could be tested independently
Don't extract everything — only extract where the name would be more honest than the current comment.
The rate limit problem
Legacy refactoring sessions are brutal on rate limits. You start with reconnaissance, build up the dependency map, write characterization tests, start the refactor — and then Claude hits its limits right when you need it most.
I solved this by switching to SimplyLouie ($2/month, flat rate). It's Claude without the per-session throttling. Long refactoring sessions stay uninterrupted. The characterization test phase alone for a big module can take 45 minutes of back-and-forth — that's when you really feel the difference between throttled and not.
The complete session checklist
Before you start a refactoring session:
- [ ] Identify the module you're going to touch
- [ ] Run existing tests (document the failures if any)
- [ ] Write characterization tests for the module
- [ ] Map its callers (who depends on this interface?)
- [ ] Set a blast radius limit: what's the maximum number of files you'll touch?
During the session:
- [ ] Work in one function at a time
- [ ] Run tests after every change
- [ ] Never let the code be in a broken state at the end of a working block
- [ ] Commit each small step ("extract getUserById", not "refactor auth")
After the session:
- [ ] Delete the characterization tests that are now redundant
- [ ] Update the documentation to match the new structure
- [ ] Write a short note about what you changed and why
Common mistakes
Don't rename and refactor at the same time. Pick one. Renaming is cheap. Refactoring is expensive. Doing both at once makes diffs unreadable.
Don't trust your intuition about what's safe to change. That's what characterization tests are for. Write them first, always.
Don't refactor the wrong layer. If the real problem is architectural (the service layer is doing database work), fixing function names won't help. Map the layers first.
Results from my last session
I used this workflow on a 4-year-old Express codebase:
- Started: 47 files, avg 340 lines each, 0 tests
- Finished phase 1-3 in 3 hours of Claude sessions
- Result: 89 files (smaller, focused), 94% test coverage on refactored modules
- Production: zero incidents during rollout
The reconnaissance phase was the unlock. Understanding before touching saved at least a week of debugging.
Building AI tools that don't break your flow — SimplyLouie is $2/month with no session limits.
Top comments (0)