Skip to content

Refactoring

AI-assisted refactoring techniques -- safety practices, common patterns, and step-by-step workflows for restructuring code with confidence.

Claude Code excels at refactoring because it can read the entire codebase, find all call sites, update every reference, and verify tests still pass — all in one flow. The engineer’s job is to decide what to refactor and verify the result. Claude handles the tedious, error-prone parts.


The Golden Rule

Always have tests before refactoring. If the code does not have tests, write them first:

Read lib/report_generator.rb and write characterization tests that
capture its current behavior. Don't change the implementation -- just
lock down what it does today so I can refactor safely.

Once tests pass, THEN refactor. If any test fails after refactoring, you changed behavior — and that is a bug, not a refactoring.


The Safe Refactoring Workflow

1. Ensure tests exist (write characterization tests if needed)
2. Make ONE refactoring move
3. Review the diff
4. Run the tests
5. Commit
6. Repeat

One move at a time. Commit after each. If anything breaks, you can revert just the last change.

Step 1: Ensure Tests Exist

What's the test coverage for this file? List the tested and
untested behaviors.

If coverage is low:

Write characterization tests that capture the current behavior of
[method]. Don't clean up the code -- just lock down what it does.

Step 2: Make One Refactoring Move

Apply Extract Method to [specific code]. Keep all tests passing.

Step 3: Review with /diff

/diff

Verify that Claude changed only what you asked for.

Step 4: Run Tests

Run the tests to verify nothing broke.

Step 5: Commit

Commit with message: "refactor: extract validation helpers from register method"

Step 6: Repeat

Next move. One at a time. Commit after each.


Common Refactoring Patterns

Pattern 1: Extract Method

When to use: A method is too long, does multiple things, or has deeply nested logic.

The generate_report method in report_generator.rb is 180 lines long.
Extract the data processing logic for each report type into separate
private methods. Keep the main method as a dispatcher that calls the
right one. Don't change any behavior -- all existing tests must pass.

After Claude makes the changes, verify:

  • The main method is now short and readable
  • Each extracted method has a clear name that describes what it does
  • No logic was accidentally changed during extraction
  • Tests still pass

Iterative extraction — don’t try to refactor everything at once:

Now extract the formatting logic from the daily_summary_data method
into a separate TextFormatter class. Keep the same output.

Test after each step. If something breaks, you know exactly which change caused it.

Pattern 2: Replace Conditional with Polymorphism

When to use: Long if/elsif/else chains or switch statements that choose behavior based on a type.

Identify the smell first:

Read this file and identify any long conditional chains that could be
replaced with polymorphism or a strategy pattern. Show me which
conditions would become which classes.

Claude might identify:

if format == "text"    -> TextFormatter
elsif format == "csv"  -> CsvFormatter
elsif format == "html" -> HtmlFormatter

Then refactor:

Replace the format conditional in generate_report with a strategy
pattern. Create TextFormatter, CsvFormatter, and HtmlFormatter classes
that each implement a format(data) method. The output must be identical
to the current implementation.

The phrase “output must be identical” is critical. Without it, Claude might “improve” the HTML output or change CSV delimiters. You want a pure refactoring — same behavior, better structure.

Pattern 3: Introduce Parameter Object

When to use: A method takes too many parameters, or the same group of parameters appears in multiple methods.

Find methods in this codebase that take more than 4 parameters.
For each one, suggest a parameter object that would group related
parameters together.

Then:

The processOrder method takes (userId, productId, quantity, price,
discountCode, shippingAddress, billingAddress). Create an OrderRequest
class that groups these, and update processOrder to accept it.
Update all callers.

Claude updates all the callers — this is where AI shines. Finding and updating every call site is tedious for humans but trivial for Claude.

Pattern 4: Simplify Nested Conditionals

When to use: Code has 3+ levels of nesting, making it hard to follow the logic.

Before:

public void process(Order order) {
    if (order != null) {
        if (order.isValid()) {
            if (order.hasStock()) {
                // actual logic buried 3 levels deep
            } else {
                throw new OutOfStockException();
            }
        } else {
            throw new InvalidOrderException();
        }
    } else {
        throw new IllegalArgumentException("order is null");
    }
}

The prompt:

Flatten the nested conditionals in the process method using guard
clauses (early returns). Keep the same behavior -- all error cases
should still throw the same exceptions.

After:

public void process(Order order) {
    if (order == null) throw new IllegalArgumentException("order is null");
    if (!order.isValid()) throw new InvalidOrderException();
    if (!order.hasStock()) throw new OutOfStockException();

    // actual logic at top level, easy to read
}

Pattern 5: Remove Dead Code

When to use: You suspect there is code that is no longer called.

Search the codebase for any methods in UserService.java that are
never called by any other file. List them with their line numbers.

Then:

Remove the unused methods you identified. Verify nothing references
them by checking imports and test files too.

Caution: Claude might miss reflection-based calls or dynamic dispatch. If the code uses runtime class loading:

Some of these methods might be called via reflection. Only remove
methods that are clearly dead -- skip anything that looks like it
could be a plugin hook or callback.

Pattern 6: Extract Class (Split God Objects)

When to use: A class has too many responsibilities — it handles data access, business logic, and formatting all in one place.

Get a proposal first:

This ReportGenerator class handles data processing, score calculation,
and output formatting for 3 report types and 3 formats. That's too
many responsibilities. Suggest how to split it into focused classes.
Don't make any changes yet -- just show me the proposed structure.

Review the proposal, then implement incrementally:

That plan looks good. Start with extracting the formatters into
separate classes. Do one class at a time and run tests after each.

Complete Walkthrough: Refactoring Before Building a Feature

Scenario: You need to add BOGO (buy-one-get-one) discounts, but the discount logic is tangled into a 400-line method.

Step 1: Assess the Current State

Read @src/OrderProcessor.java. I need to add a new discount type
(buy-one-get-one), but this class is too tangled. What's the minimum
refactoring needed to make it safe to add a new discount type?
Don't suggest a full rewrite -- just the targeted extraction.

Step 2: Refactor First

Extract the discount logic (lines 80-115) into a new DiscountEngine
class. The OrderProcessor should call DiscountEngine.calculate().
Keep behavior identical -- no new features yet. Run all existing tests.

Step 3: Verify the Refactor

Run the full test suite. Show me the test results and confirm
nothing changed in behavior.

Step 4: NOW Add the Feature

Now add BOGO (buy-one-get-one-free) support to the DiscountEngine.
The BOGO discount gives the cheapest item free when buying 2+ of
the same product. Write tests for it first, then implement.

Why this sequence matters:

  • Step 2 is a pure refactor — behavior does not change, tests still pass
  • Step 4 adds new behavior to clean code — much lower risk
  • If something breaks, you know exactly which step caused it

Common Refactoring Prompts

What You WantPrompt
Make code readable”Break this 80-line method into smaller methods with descriptive names”
Remove duplication”These three methods have similar logic. Extract the shared parts into a helper”
Simplify flow”Flatten these nested ifs into guard clauses with early returns”
Fix naming”Rename variables and methods in this class to better describe what they do”
Separate concerns”Split this class into a data-processing class and a formatting class”
Replace magic values”Replace the magic numbers 90, 75, and 60 with named constants”

What Claude Gets Wrong (and How to Handle It)

Over-Refactoring

Claude sometimes refactors more than you asked. If you said “extract this method,” Claude might also rename variables, reorder imports, and add type annotations.

Fix: Be explicit: “Only extract the method. Don’t change anything else.”

Changing Behavior During Refactoring

Sometimes Claude “fixes” a bug while refactoring. This is dangerous — you want the refactoring to be pure.

Fix: “Don’t fix any bugs during this refactoring. Keep the exact same behavior, even if you see issues. We’ll fix bugs separately.”

Creating Unnecessary Abstractions

Claude might introduce interfaces, abstract classes, or generics that add complexity without value.

Fix: “Keep it simple. Don’t add abstractions unless there are at least 2 concrete implementations that need them.”


Safety Practices Summary

PracticeWhy It Matters
Tests before refactoringYou need a safety net to catch behavior changes
One move at a timeSmall changes are easy to review and revert
Commit after each moveYou can always get back to a working state
Review every diffClaude may change more than you asked for
”Keep behavior identical”Prevents Claude from mixing refactoring with feature work
Run tests after every changeCatch regressions immediately, not 5 changes later