DEV Community

brian austin
brian austin

Posted on

How I use Claude Code to refactor legacy code — without breaking everything

How I use Claude Code to refactor legacy code — without breaking everything

Legacy code is the bane of every developer's existence. You inherit a 10,000-line file with zero tests, cryptic variable names, and comments like // this is important - don't touch. Then your manager says: "Clean this up by Friday."

Here's the workflow I've developed for using Claude Code to safely refactor legacy code — without introducing new bugs or spending a week on it.

The problem with legacy refactoring

Legacy code refactoring fails for predictable reasons:

  • You don't understand what the code actually does
  • You break things silently (no tests to catch it)
  • You run out of context window mid-refactor
  • The codebase is too big to load at once

Claude Code makes all of these solvable — if you use it systematically.

Step 1: Map before you touch anything

Before changing a single line, I have Claude build a map of the code.

Read src/legacy/payments.js and:
1. List every function and what it does in one sentence
2. Identify all external dependencies (imports, API calls, DB queries)
3. Find all places where state is mutated
4. Mark functions that have side effects
5. Note anything that looks like business logic (not infrastructure)
Enter fullscreen mode Exit fullscreen mode

This gives you a dependency inventory — you know what's safe to move and what's load-bearing.

Step 2: Write characterization tests first

Before refactoring, lock in the current behavior:

Look at the processPayment() function in payments.js.
Write Jest tests that document its CURRENT behavior — not what it should do, what it ACTUALLY does right now.
Include edge cases: null inputs, missing fields, error states.
Don't fix bugs — capture them. We'll address them after refactoring.
Enter fullscreen mode Exit fullscreen mode

These are called characterization tests — they're not about correctness, they're about safety. If your refactor breaks them, you know you changed behavior.

Step 3: Extract the obvious wins first

Start with the lowest-risk refactors:

In payments.js, identify:
1. Pure utility functions (no side effects, no external dependencies)
2. Duplicate code blocks (>5 lines repeated)
3. Magic numbers and strings that should be constants

For each utility function found, extract it to src/utils/payment-utils.js
Do NOT change the function logic — exact copy, just moved.
Update all import references.
Enter fullscreen mode Exit fullscreen mode

Pure functions with no dependencies are safe to move. You're not changing behavior, just organization.

Step 4: Tackle the god function

Every legacy codebase has a god function — 200+ lines that does everything. Here's how to break it down:

The processPayment() function is 340 lines long.
Identify 3-5 natural "phases" within it — distinct operations that happen in sequence.
For each phase:
- Name it clearly
- Extract it into a separate function
- The extracted function should receive data via parameters (no implicit state)
- Return values rather than mutating external state

Do this one phase at a time. After each extraction, run the characterization tests.
Enter fullscreen mode Exit fullscreen mode

One extraction at a time. Run tests after each one. Never batch-refactor a god function.

Step 5: Rename with context

Once the structure is clear, rename everything:

Review all variable names in processPayment() and its extracted functions.
For each unclear name (single letters, abbreviations, generic names like 'data', 'result', 'temp'):
- Look at how it's used in context
- Suggest a name that describes WHAT it is, not what it does
- Show me before/after

Don't auto-rename — show me the list first.
Enter fullscreen mode Exit fullscreen mode

I always review rename suggestions before applying. Wrong renames introduce bugs that are hard to find.

Step 6: Add JSDoc while Claude has context

This is the best time to document — while the code is fresh in context:

For each function we've extracted and renamed, write JSDoc comments that include:
1. What the function does (one sentence)
2. @param for each parameter with type and description
3. @returns with type and description
4. @throws if it can throw errors
5. @example with a real usage example

Base the documentation on actual behavior you've observed, not what the name implies.
Enter fullscreen mode Exit fullscreen mode

Step 7: Delete dead code

Review payments.js for:
1. Functions that are never called anywhere in the codebase (search all .js files)
2. Commented-out code blocks older than the newest git commit date
3. Conditional branches that can never be reached
4. Feature flags set to constant false

List what you find — don't delete yet. I'll review before we remove.
Enter fullscreen mode Exit fullscreen mode

Dead code is a trap. Delete it with ceremony — list it, review it, THEN delete.

The context management problem

Here's the hidden challenge with legacy refactoring: it generates massive amounts of context.

A typical legacy refactor session involves:

  • Reading 5-10 files to understand dependencies
  • Multiple rounds of test writing
  • Iterative extractions with test runs between each
  • Reviewing rename suggestions

Each of these steps burns tokens. A serious legacy refactor session regularly hits Claude's rate limits — sometimes multiple times in a single afternoon.

I've been using SimplyLouie as my ANTHROPIC_BASE_URL for Claude Code. It's ✌️$2/month with no rate limits — when Claude Code says "I've reached my limit, try again in an hour," I'm not blocked.

export ANTHROPIC_BASE_URL=https://api.simplylouie.com
Enter fullscreen mode Exit fullscreen mode

7-day free trial, no commitment. Worth it for any session involving a real legacy refactor.

The complete workflow at a glance

  1. Map — understand before touching
  2. Characterize — lock in current behavior with tests
  3. Extract utilities — low-risk, pure functions first
  4. Break god functions — one phase at a time, test after each
  5. Rename — with context, review before applying
  6. Document — while context is fresh
  7. Delete dead code — list, review, then remove

What this looks like in practice

I recently used this workflow on a 2,400-line api-handler.js file that had been accumulating since 2018. The full process took two 3-hour sessions:

  • Session 1: Map + characterization tests + utility extraction
  • Session 2: God function breakdown + rename + documentation

The file went from 2,400 lines to five files averaging 180 lines each. All 47 characterization tests still pass. The team can now actually read the code.

Don't skip the characterization tests

Every time I've skipped this step, I've regretted it. Legacy code has bugs that downstream code depends on. If you fix those bugs during refactoring, you break the downstream code — and you won't know why because there were no tests capturing the original behavior.

Characterization tests feel like extra work upfront. They save hours of debugging later.


Working with a particularly gnarly legacy codebase? Drop your specific challenge in the comments — happy to share approaches for specific patterns.

Top comments (0)