Tutorials10 min read

How to Refactor Legacy Code with Claude Code: A Developer's Practical Guide

Step-by-step guide to refactoring legacy code using Claude Code. Learn how to understand tangled codebases, write characterization tests, and modernize code safely with AI assistance.

How to Refactor Legacy Code with Claude Code: A Developer's Practical Guide

Legacy code is every developer's most-avoided task. A 300-line function with no tests, cryptic variable names, and business logic baked into database queries — you know the type. It works, so nobody wants to touch it. But it slows down every new feature and makes debugging a nightmare.

Claude Code changes the calculus. With the right workflow, you can safely understand, test, and refactor code you didn't write — in a fraction of the time it used to take. This guide walks through the exact process, with concrete prompts and examples at each step.

Why Refactoring with AI Is Different (Not Just Faster)

The hardest part of refactoring isn't writing the new code — it's understanding what the old code actually does, including the undocumented edge cases, the implicit assumptions, and the "shadow logic" hiding in conditionals nobody remembers writing.

This is where Claude genuinely excels. It can read a function, trace its execution paths, and explain what it does in plain English before you touch a single line. Teams using Claude for refactoring tasks report 55–80% productivity gains, with the biggest win coming from the understanding phase, not the rewriting phase.

But AI assistance requires discipline. Claude will generate code confidently even when it misses context. The framework below keeps you in control while letting Claude do the heavy lifting.

What You Need Before Starting

  • Claude Code installed and running (npm install -g @anthropic-ai/claude-code or the desktop app)
  • A codebase in version control — never refactor without a clean Git state
  • Basic familiarity with your testing framework (Jest, pytest, RSpec, etc.)

For Claude Code setup help, see our Getting Started with Claude Code guide.


Step 1: Let Claude Map the Territory

Before changing anything, you need to understand what you're dealing with. Open Claude Code in your project root and ask it to analyze the target file or module.

You: Read src/billing/invoice-processor.ts and explain:
1. What does this module do?
2. What are its dependencies (imports, database calls, external services)?
3. What are the entry points — which functions are called from outside this file?
4. What side effects does it have?
5. What looks most fragile or tightly coupled?

Claude will return a structured breakdown. Pay attention to the side effects and dependencies sections — these are your refactoring landmines. If Claude says "this function writes to three different database tables and sends an email," that's a sign you need characterization tests before touching anything.

What to look for in Claude's analysis:
  • Functions doing more than one thing (violates single responsibility)
  • Deeply nested conditionals (cyclomatic complexity > 10 is a red flag)
  • Database or I/O calls mixed with business logic
  • Magic numbers and unexplained string literals
  • Functions longer than 50 lines


Step 2: Write Characterization Tests First

Characterization tests are a safety net that captures what the code currently does, not what it should do. They're not about correctness — they're about ensuring refactoring doesn't change behavior.

You: Based on your analysis of invoice-processor.ts, generate characterization tests 
that cover its current behavior, including edge cases. Use Jest. Focus on:
- The main processInvoice() function
- What it returns for valid input
- What it returns or throws for invalid input
- Any internal state changes

Don't write tests for what it "should" do — write tests that document exactly 
what it does today, even if that behavior seems wrong.

Run these tests before refactoring. They should all pass (they're describing current behavior). After refactoring, run them again — if any fail, you accidentally changed behavior.

Sample output from Claude:

typescript// characterization.test.ts — auto-generated by Claude Code
describe('processInvoice() — characterization', () => {
  it('returns null for invoices with zero line items (current behavior)', async () => {
    const result = await processInvoice({ lineItems: [], customerId: '123' });
    expect(result).toBeNull(); // Current behavior: silently returns null
  });

  it('applies 15% tax to all US customers regardless of state', async () => {
    const result = await processInvoice({
      lineItems: [{ amount: 100, description: 'Service' }],
      customerId: 'us-customer-456',
    });
    expect(result.tax).toBe(15); // Hardcoded 15% — likely a bug, but capture it
  });
});

Notice how Claude flagged "likely a bug" — that's the kind of insight you get from having it analyze before writing tests.


Step 3: Break Down Complex Functions

Now you're ready to refactor. Start with the most complex function — usually the one everyone avoids. Ask Claude to decompose it without changing its external interface.

You: Refactor the processInvoice() function by extracting sub-functions. Rules:
1. Don't change the public signature — same inputs, same outputs
2. Each extracted function should do exactly one thing
3. Keep all the characterization tests passing
4. Name functions clearly — no abbreviations
5. Add JSDoc to each new function explaining what it does and why

Show me the refactored code as a diff so I can review before applying.

Asking for a diff is a critical habit. It forces Claude to show you exactly what changed, rather than returning a full rewrite you have to compare mentally. Review every line before accepting.

Before (a real refactoring example from the Claude Code docs):

typescript// 210 lines, cyclomatic complexity: 16
async function processInvoice(data: any) {
  if (data && data.lineItems && data.lineItems.length > 0) {
    let total = 0;
    for (let i = 0; i < data.lineItems.length; i++) {
      if (data.lineItems[i].discount) {
        total += data.lineItems[i].amount * (1 - data.lineItems[i].discount);
      } else {
        total += data.lineItems[i].amount;
      }
    }
    // ... 180 more lines
  }
}

After Claude's refactoring:

typescript// 3 focused functions, cyclomatic complexity: 3-4 each
function calculateLineItemTotal(item: LineItem): number {
  return item.discount ? item.amount * (1 - item.discount) : item.amount;
}

function calculateSubtotal(lineItems: LineItem[]): number {
  return lineItems.reduce((sum, item) => sum + calculateLineItemTotal(item), 0);
}

async function processInvoice(data: InvoiceInput): Promise<ProcessedInvoice | null> {
  if (!data?.lineItems?.length) return null;
  const subtotal = calculateSubtotal(data.lineItems);
  // ... continued refactoring
}

This is more readable, testable, and maintainable — and the characterization tests still pass.


Step 4: Improve Type Safety and Error Handling

Once the structure is clean, tackle type safety. This is especially important for TypeScript codebases converted from JavaScript, or Python code that grew organically without type hints.

You: Now that the functions are separated, improve type safety:
1. Replace all `any` types with proper interfaces
2. Add input validation at the entry point with descriptive error messages
3. Make the return type explicit — no implicit returns
4. Use discriminated unions for success/error results instead of try/catch

Keep the same behavior — just make the types honest.

Prompt for Python projects:

You: Add type hints to all functions in invoice_processor.py. Use:
- TypedDict for dictionary types
- Optional[T] only where None is a real valid return value  
- Union types for cases where multiple types are genuinely needed

Flag any place where the current code uses a dict when it should be a dataclass.


Step 5: Handle Multi-File Refactoring with Claude Code Subagents

When refactoring touches multiple files — moving a function to a shared utility, updating all callers, updating tests — Claude Code's subagent capabilities shine. This is one of the most powerful patterns for larger refactoring tasks.

You: The validateCustomer() function in invoice-processor.ts is used in 
6 other files. I want to:
1. Extract it to src/shared/customer-validation.ts
2. Update all 6 callers to import from the new location
3. Delete it from invoice-processor.ts
4. Update the tests

Do a global search first to confirm the 6 files, then make the changes.

Claude Code will use its file search tools to find all usages, make the moves atomically, and update imports. What would take 30+ minutes of manual work — grepping, editing each file, fixing import paths — happens in 2-3 minutes.

For more on subagent capabilities, see our Claude Code Subagents guide.


Comparison: Manual vs. Claude-Assisted Refactoring

TaskManual TimeWith Claude CodeTime Saved
Understanding a 200-line function45–60 min5–10 min~85%
Writing characterization tests60–90 min15–20 min~75%
Breaking down a complex function30–60 min10–15 min~70%
Updating all callers after extraction20–40 min3–5 min~88%
Adding TypeScript types30–45 min8–12 min~75%
Full refactor of a 200-line module3–5 hours45–75 min~75%

Common Mistakes to Avoid

1. Skipping the analysis step.

Jumping straight to "refactor this function" without first asking Claude to explain it leads to subtle bugs. Claude may restructure code without understanding that a particular order-of-operations was intentional.

2. Not reviewing diffs.

Accept large rewrites without reviewing and you'll inherit whatever mistakes Claude made. Always request diffs for changes > 20 lines.

3. Letting context window fill up.

Claude's quality degrades past 60% of its context window. For large refactoring sessions, use /clear to reset, save your progress to a markdown file, and continue fresh. The Claude Code context engineering guide covers this in detail.

4. Refactoring and adding features simultaneously.

One at a time. Refactoring first (behavior-preserving), then new features (behavior-changing). Mixing the two makes it impossible to debug regressions.

5. Trusting Claude's output on security-sensitive code.

For authentication, authorization, cryptography, and payment processing code, treat Claude as a first pass only. Have a human expert review any changes to these areas.


Advanced Pattern: Incremental Refactoring with CLAUDE.md

If you're doing a long refactoring project across multiple sessions, track progress in your CLAUDE.md file so Claude has context each time:

markdown## Active Refactoring: Billing Module

**Status:** In progress
**Goal:** Extract business logic from database layer in src/billing/

**Completed:**
- [x] invoice-processor.ts — extracted to 5 focused functions
- [x] Characterization tests added (billing.characterization.test.ts)

**In progress:**
- [ ] payment-gateway.ts — 340 lines, needs analysis first

**Rules for this refactor:**
- External interfaces must not change (downstream service depends on them)
- All DB access must go through the repository pattern
- No new dependencies without discussion

This pattern lets you pause and resume refactoring across days without losing context or accidentally re-doing completed work.


Key Takeaways

  • Analyze before refactoring — ask Claude to explain the code, map dependencies, and surface shadow logic before writing a single line of new code
  • Characterization tests are non-negotiable — they're your safety net and they cost 15 minutes, not 90
  • Request diffs, not full rewrites — review what changed rather than trying to compare whole files
  • Use subagents for cross-file changes — moving functions and updating all callers is a prime use case for Claude Code's agentic tools
  • Manage context actively — clear the window before it fills, save progress to markdown


Start Applying This Today

The best way to learn this workflow is to pick one function you've been avoiding and run through the five steps above. Start with the analysis prompt — just understanding what the code does is often valuable on its own.

If you want to go deeper on using Claude for your development workflow, check out the AI for Anything Certification Prep resources. Our Claude Certified Architect (CCA-F) practice tests and study guides cover Claude Code, agents, and the Anthropic API — everything you need to move from occasional Claude user to certified expert.


Want to see more Claude Code tutorials? Browse our full guide collection covering prompt engineering, multi-agent patterns, MCP servers, and more.

Ready to Start Practicing?

300+ scenario-based practice questions covering all 5 CCA domains. Detailed explanations for every answer.

Free CCA Study Kit

Get domain cheat sheets, anti-pattern flashcards, and weekly exam tips. No spam, unsubscribe anytime.