Refactoring Patterns
You open src/services/checkout.service.ts to add one discount rule and find a single processOrder function that is 340 lines long: inline validation, three nested if branches for payment type, a pricing calculation, an inventory write, and an email send — all sharing local variables. The existing Vitest suite is green, but nobody wants to touch it because any change might silently break a path that only fires in production. This is the exact situation where Claude Code earns its keep: it can characterize the current behavior in tests first, then move code in small, reversible steps you can verify after each one.
What You Will Walk Away With
Section titled “What You Will Walk Away With”- A test-first refactoring loop that keeps a green suite at every step
- A copy-paste Extract Method prompt that names real files and runs your suite to prove behavior is unchanged
- A prompt that replaces a payment-type
switchwith the Strategy pattern and a factory - A legacy-modernization prompt (callbacks to
async/await) that updates call sites and tests together - The failure modes that actually bite during AI-assisted refactoring, and how to recover
The Refactoring Loop
Section titled “The Refactoring Loop”Refactoring with Claude Code is not “ask it to clean this up.” It is a disciplined loop where tests are the contract and each step is small enough to review:
-
Pin the behavior. Generate characterization tests for the current code so any behavior change shows up as a red test. If the area already has good coverage, skip to step 2.
-
Make one named transformation. Extract a method, introduce a parameter object, replace a conditional — one move per turn, not a rewrite.
-
Verify. Run the test suite, the type checker, and the linter. Green means the move was behavior-preserving.
-
Commit, then repeat. One refactoring per commit so a bad step is a one-line
git revert, not an archaeology project.
The non-obvious part is step 1. Ask Claude to read the real file before it writes anything, so it reasons about the code that exists rather than the code it assumes exists.
Extract Method: The Workhorse
Section titled “Extract Method: The Workhorse”Most “this function is too long” problems are solved by extracting cohesive blocks into named functions. The win with Claude Code is that it threads the shared local variables correctly and updates the original to call the new functions — the tedious, error-prone part.
Show it the same concept in your language. The interaction is identical; only the idioms differ:
In src/services/checkout.service.ts, processOrder does five jobs. Extract threepure functions in the same file: validateOrder(input), calculatePricing(cart),and reserveInventory(items). Leave the email send and the DB write inprocessOrder as the orchestrator. Keep every existing type; do not widen anytype to `any`. Then run `npx vitest run checkout` and `npm run type-check`and report both results.In app/services/checkout.py, refactor process_order by extractingvalidate_order(payload), calculate_pricing(cart), and reserve_inventory(items)as module-level functions with type hints. Keep process_order as theorchestrator. Run `pytest tests/test_checkout.py -q` and `mypy app/services`and show me the output before and after.In lib/shop/checkout.ex, process_order/1 is doing too much. Extractvalidate_order/1, calculate_pricing/1, and reserve_inventory/1 as privatefunctions, and pipe them together in process_order/1. Keep the existingtypespecs. Run `mix test test/shop/checkout_test.exs` and report the result.That last line matters. The most common failure mode in AI refactoring is the model editing a test to make it pass. Telling it to revert and report instead keeps the test as the source of truth.
Replace Conditional with Polymorphism
Section titled “Replace Conditional with Polymorphism”A payment-type switch that grows a new branch every quarter is a maintenance tax. The Strategy pattern moves each case behind a common interface. This is a multi-file change where order matters, so run it in Plan Mode first (Shift+Tab to cycle into Plan Mode) and approve the plan before any edits.
The behavioral test of a good Strategy refactor is simple: the existing payments test suite should pass with zero edits, because you only moved logic, you did not change it. If a test needs editing, the refactor changed behavior and you should stop.
Modernize Legacy Code
Section titled “Modernize Legacy Code”Callback-based APIs, .then() chains, and CommonJS modules are the usual legacy targets. The trap is updating the implementation but leaving call sites or tests on the old shape. Make Claude update them together.
A current, realistic framework target is a React 18-to-19 migration. React 19 is the current stable major, so anchor on it rather than an older jump:
Create a migration plan to move this app from React 18 to React 19:- Identify uses of the removed legacy APIs (ReactDOM.render, findDOMNode, legacy Context, string refs) with Grep and list each occurrence.- Map any forwardRef components that can drop the wrapper now that ref is a prop.- Note where the `use()` hook or Actions would simplify existing data-fetching.Do not edit anything yet. Output the plan as a checklist I approve before youstart, smallest-risk items first.Splitting a Fat Service
Section titled “Splitting a Fat Service”When a class has absorbed unrelated responsibilities, split it — but verify the seams with the type checker, not by eye.
src/services/user.service.ts has grown to ~900 lines and mixes authentication,profile updates, and notification sending. Plan a split into AuthService,ProfileService, and NotificationService. For each: list which existing methodsmove, which private helpers they need, and which call sites must be updated.Keep one thin UserService facade that delegates, so external imports do notbreak in this PR. Implement only after I approve the plan, one service percommit. Run `npm run type-check` after each service to catch a missed reference.When This Breaks
Section titled “When This Breaks”Claude edits the test to make it pass. This is the cardinal failure. Always include “if a test fails, revert your change and report which assertion broke” in the prompt, and review the diff for test-file edits you did not ask for. The test is the contract; the implementation moves around it.
The refactor “works” but a production-only path broke. Your characterization tests did not cover that path. Before refactoring high-stakes code, ask Claude to enumerate the branches first (“List every distinct code path through processOrder and the input that triggers it”) and write a test for each before touching anything.
A big-bang change lands as one giant diff. Stop and reset: git reset --hard <last-known-good-commit>. Then re-run the work with an explicit “one refactoring per commit, run the suite between each” instruction. Small steps are what make AI refactoring safe to review.
The model loses the codebase’s conventions mid-refactor. It started inventing a new error format or a different folder layout. Point it back at a concrete anchor: “Match the error-handling pattern in src/api/routes/orders.ts exactly — same error class, same response shape.” Encoding these conventions in your CLAUDE.md keeps every session consistent.
Performance “optimization” changes results. If you asked it to replace an O(n^2) loop, treat it as a refactor under test: pin the output with a test first, swap the algorithm, then confirm the test still passes and benchmark with real numbers (console.time or your bench harness) rather than trusting an asymptotic claim.