Refactoring a Legacy Module with Claude Code
Legacy modules usually contain more business knowledge than their structure suggests. An awkward conditional may preserve an old customer agreement; inconsistent rounding may already appear on invoices. Refactoring safely means improving the structure without quietly rewriting those rules.
This walkthrough uses Claude Code to untangle a hypothetical invoice module that mixes calculations, database access, and email delivery. The goal is a reviewable change that preserves observable behavior.
§ 1. Prepare a clean working environment
Create a dedicated branch and check the working tree before starting Claude Code. Commit or set aside unrelated changes, install dependencies, and run the existing tests. Record failures that already exist so they are not mistaken for regressions.
If you use Vana API, create an account and obtain an API key. Its endpoint, https://api.vanaapi.com, supports the Anthropic Messages protocol used by Claude Code, alongside OpenAI-compatible and Gemini native protocols. Configure Claude Code's ANTHROPIC_BASE_URL and the authentication variable specified in your Vana API setup instructions. Keep credentials outside the repository.
Vana API bills per real token with no monthly fee. That makes focused sessions useful for controlling both review scope and usage.
§ 2. Ask for a behavior map, not a patch
Start with inspection. Give Claude Code a narrow target and explicitly prohibit edits:
Prompt: Inspect the invoice module and its direct callers. Do not edit files. Map its public interface, side effects, dependencies, and existing tests. Identify behavior that could change accidentally during refactoring. Cite file paths and relevant functions.
Check the report against the code yourself. Pay particular attention to exported functions, database writes, email ordering, error types, and implicit defaults. A confident summary is not evidence that every dependency has been found.
§ 3. Define the refactoring contract
Write down what must remain unchanged before discussing a cleaner design. For this example, the contract might include:
- Public function signatures and returned object fields stay unchanged.
- Discounts, tax calculations, and rounding follow the existing sequence.
- An invoice is persisted before its notification is sent.
- Existing validation errors retain their types and messages.
- No dependency upgrades or schema changes are included.
Put durable repository guidance in CLAUDE.md if your project uses it. Keep task-specific acceptance criteria in the session. Ask Claude Code to flag ambiguities rather than resolve them by inventing new business rules.
§ 4. Add characterization tests
Characterization tests document what the module currently does, including behavior that looks undesirable. Ask Claude Code to propose cases first, then implement the approved tests without changing production code.
Cover ordinary invoices, empty line items, missing optional fields, discount boundaries, and fractional prices. Mock external boundaries to observe persistence and notification calls. Where ordering matters, assert the sequence rather than merely checking that both calls happened.
Prompt: Add tests that capture current behavior under this contract. Do not correct suspected bugs. Report surprising outputs separately, and show the test command and its result.
§ 5. Extract one responsibility
Choose the smallest useful seam. Here, extract calculation logic into a pure helper while leaving database access and email delivery in the original function.
Prompt: Extract invoice calculation into a helper. Preserve arithmetic order, coercion, rounding, and returned fields exactly. Keep the public interface unchanged. Avoid unrelated renaming or formatting. Run the focused tests afterward.
Review the diff before requesting another change. A mathematically equivalent expression can behave differently with floating-point values, so moving calculations is safer than rewriting them during the same step.
§ 6. Validate beyond the focused tests
Run the module tests, then the broader test suite, type checker, and linter available in the repository. Verify command results rather than accepting a summary that says everything passed. If a command cannot run, record the limitation explicitly.
Inspect the diff for changed exports, new dependencies, weakened assertions, swallowed exceptions, and accidental edits to fixtures. Ask Claude Code to compare the patch against the original contract and identify possible violations.
§ 7. Commit the seam, then continue
Commit the extraction with a short explanation of what moved and which checks passed. Only then consider separating persistence from notification orchestration.
Keep suspected bug fixes in separate changes with their own expected behavior and tests. The successful outcome is not the largest rewrite. It is a smaller, clearer module whose behavior remains supported by evidence.