12 Prompts for Refactoring Legacy Code: Strategies and Examples
Legacy code is everywhere. It's the codebase that powers your business but terrifies your developers. Michael Feathers, author of Working Effectively with Legacy Code, defined legacy code simply as "code without tests." That definition is liberating — it means the fastest way to make any codebase less legacy is to add tests. But before you can write tests, you need to understand the code. This is where large language models (LLMs) like GPT-4, Claude, or GitHub Copilot can help.
This article is not about blindly asking an AI to "fix" your code. It's a curated collection of 12 practical prompts that target specific refactoring strategies. Each prompt is a starting point — you'll need to adapt it to your codebase, language, and constraints. We'll cover everything from understanding existing behavior to breaking dependencies and introducing test seams.
Important: always treat AI suggestions as a baseline. Never pipe production code into a public AI service without checking your company's data policy. Use self-hosted models or local tools like Ollama if you're dealing with sensitive source code.
How to get the most from refactoring prompts
Before we dive into prompts, here are three principles that make the difference between a generic answer and an actionable refactoring plan:
- Provide context. Paste the specific function, class, or module you're working on. Add a short description of its purpose and the surrounding architecture.
- Define constraints. Tell the model which patterns you want to keep, which ones you're moving away from, and which coding standards you follow.
- Iterate. The first answer is never final. Ask follow-up questions: "How would this change affect error handling?" or "Can you show me a version with fallback logic?"
With those ground rules in place, let's look at the prompts.
The 12 Prompts
1. The Legacy Code Audit Prompt
Task: Get a structured high-level assessment of a legacy file or module before touching anything.
Prompt:
You are a software architect with 15 years of experience in refactoring legacy systems. Analyze the following code. Provide a structured report in Markdown with these sections:
1. **Current behavior** – what the code does, in plain English.
2. **Code smells** – list any design problems (God class, feature envy, duplicated logic, etc.). For each smell, give a one-sentence explanation.
3. **Risks** – what could break if we refactor this module? Mention external dependencies, global state, concurrency, etc.
4. **Recommended refactoring strategy** – suggest a step-by-step plan, ordered by risk level (low-risk first), and identify which patterns from "Working Effectively with Legacy Code" apply.
Here is the code:
[PASTE CODE]
Why it works: The prompt forces the model to separate "what it does" from "what it should do." This helps the human review the AI's understanding before any change is made.
Use case: I used a variant of this on a 20-year-old C++ class that handled both network I/O and XML parsing. The audit correctly identified the single responsibility violation and suggested extracting the XML parser into a separate component. The plan started with adding a logging wrapper to capture current behavior — a classic characterization test approach.
2. The "Write Characterization Tests First" Prompt
Task: Create tests that lock in the current behavior so you can refactor safely.
Prompt:
You are a test engineer. Write characterization tests for the following legacy function. Characterization tests should verify the CURRENT output of the function for a set of inputs, without asserting on expected "correct" behavior — we just want to document what the code actually does.
1. Read the function and identify its public interface.
2. Generate 5–10 test cases that cover the main branches, edge cases (empty input, null, zero, malformed data), and likely call paths.
3. For each test, write an assertion that captures the actual output as-is. Add a comment showing what the output is.
4. If the function has side effects (files, global variables), design a test that observes those side effects.
Use the xUnit framework for [language/framework] (e.g., JUnit, NUnit, pytest).
Here is the function:
[PASTE CODE]
Why it works: This is the core of the Feathers method. Once you have characterization tests, you can refactor with confidence.
Use case: On a legacy Java payment gateway, we used this prompt to generate tests for a calculateDiscount method that had no documentation. The prompt produced 7 test cases, including a surprising one: with a negative discount code, the method returned a value greater than the original price. That behavior was a bug, but it was now locked in. We fixed it later after stakeholders confirmed the correct logic.
3. The "Extract Function" Prompt
Task: Break down a long method into smaller, well-named methods.
Prompt:
You are a refactoring expert. Here is a method that is too long and does too many things. Apply the "Extract Function" refactoring (as described in Martin Fowler's Refactoring) to split it into smaller, cohesive methods.
Guidelines:
- Identify distinct steps within the method (e.g., input validation, business logic, formatting).
- Extract each step into a private method with a descriptive name.
- Preserve the exact same behavior. Note: if you change the behavior, say so.
- Keep the extracted methods at a similar level of abstraction (the "composed method" pattern).
- Show the resulting code in a code block, and provide a short explanation of why each split is an improvement.
Here is the method:
[PASTE CODE]
Why it works: LLMs are excellent at splitting code because they can see the logical boundaries within a method. The prompt includes a request to note behavior changes — this builds trust.
Use case: A Python function that imported data from a CSV, validated rows, and inserted them into a database was 120 lines. The extract prompt produced 4 small functions: read_csv, validate_row, sanitize_row, insert_record. The total size grew slightly, but the code became testable. We could then unit-test each function separately.
4. The "Replace Conditional with Polymorphism" Prompt
Task: Refactor complex switch/if-else blocks using object-oriented principles.
Prompt:
You are an OOP design expert. The following code uses a large conditional to behave differently based on a type/state. Replace the conditionals with polymorphism using the Strategy or State pattern, whichever fits better.
Requirements:
- Define an interface (or base class) that captures the variation.
- Create separate classes for each branch.
- Show how the client code changes (e.g., a factory method to select the strategy).
- Preserve the exact same behavior, including any default/else branch.
- If behavior is not identical, list the differences explicitly.
Here is the code:
[PASTE CODE]
Why it works: This is one of Fowler's most famous refactorings. The prompt pushes the model to build an object model instead of just rewriting the conditionals.
Use case: We refactored a Ruby order-processing system that had a 50-line case statement determining shipping costs per country. The prompt generated a ShippingCostStrategy interface with implementations for each country group. The factory method used a lookup hash. The result eliminated the case statement and made it easy to add new countries.
5. The "Break External Dependencies" Prompt (Seam Identification)
Task: Identify and break dependencies on external systems or global state.
Prompt:
In <filename>, the function [name] directly calls external services (e.g., database, HTTP API, file system). For each external call:
1. Identify the call and its type (IO, network, static method, singleton).
2. Propose a seam where we can inject a mock or stub. Use one of these techniques:
- Extract the call to a protected virtual method (override for testing).
- Extract a dependency interface and pass it via constructor.
- Introduce a factory method that can be overridden.
3. Show a refactored version of the function with the seam in place.
4. Write a short test snippet demonstrating how to inject a fake.
Here is the relevant code:
[PASTE CODE]
Why it works: Michael Feathers emphasizes "seams" as places where you can alter behavior without changing the code. This prompt directly asks the model to find them.
Use case: A legacy PHP module called get_user_data() directly used a global $db connection. The prompt suggested extracting get_user_data_from_db($id) as a protected method, allowing a test class to override it. That was the first realistic seam in the module, and it unlocked testing for the rest of the refactoring.
6. The "Detect Dead Code" Prompt
Task: Find unused methods, parameters, and variables in a large codebase.
Prompt:
You are a code quality analyst. Analyze the following code for dead code: things that are never called, written but never read, or otherwise unreachable.
For each suspect, provide:
- The name and location.
- Why you think it is dead (e.g., not referenced anywhere in this file, only referenced in tests).
- A confidence level (high/medium/low).
- A recommendation: remove, mark with @Deprecated, or keep (if it's a public API).
Be conservative — do not flag methods that are part of a public API used by external systems, or methods that use reflection. You do not have the full codebase, so rely on patterns like "calls itself alone" or "only calls other dead functions."
Here is the code:
[PASTE CODE]
Why it works: Dead code is a silent killer. The prompt asks for confidence levels and conservative decisions, which reduces false positives.
Use case: In a large .NET solution, we ran this prompt on a 3,000-line utility class. It flagged 12 methods as high-confidence dead code. After manual verification, we removed 9 of them. The difference was measurable: the class size dropped by 15%, and the code coverage calculation became more meaningful.
7. The "Improve Naming" Prompt
Task: Rename cryptic variables and methods without breaking the behavior.
Prompt:
You are a code readability specialist. Here is a legacy function. Rename variables, parameters, and methods to be self-documenting.
Rules:
- Preserve the behavior exactly. Do not restructure logic unless it's required for the rename.
- Use standard naming conventions for [language] (e.g., camelCase, snake_case).
- Do not rename public API methods that are called by other parts of the codebase (unless you provide a migration note).
- For each rename, give a short comment explaining why the new name is clearer.
- Return the full renamed function in a code block.
Here is the code:
[PASTE CODE]
Why it works: Poor naming is the most common complaint about legacy code. The prompt explicitly disallows behavior changes, so the diff is pure renaming and easy to review.
Use case: A JavaScript function called d($a,$b) was renamed to calculateDiscountedPrice(originalPrice, discountRate). The prompt maintained the same logic, and the code review took minutes instead of hours because the intent was now clear.
8. The "Extract Module from God Class" Prompt
Task: Split a class that does too much into separate cohesive modules.
Prompt:
You are a software architect. This class violates the Single Responsibility Principle — it's a "God class" that handles persistence, business logic, and presentation all at once.
Identify cohesive clusters of methods and fields that naturally belong together. Then:
1. Propose a new class or module for each cluster.
2. Explain the relationships between the new classes (e.g., dependency direction).
3. Show the public interface of each new class WITHOUT implementing them fully.
4. Show how the original class can be refactored to delegate to these new classes.
5. List the risks: circular dependencies, data sharing, etc.
Here is the class:
[PASTE CODE]
Why it works: This is a high-level design task that LLMs handle well because they can see the whole class. The output is a map, not a full implementation, which is perfect for planning.
Use case: We used this prompt on a 2,000-line C# class called ReportGenerator. The prompt split it into three modules: ReportDataFetcher, ReportRenderer, and ReportFormatter. The original class became a facade that composed the three. The refactoring took three days, but the new code shipped with a unit-test coverage of 60% (was 5%).
9. The "Convert Legacy Error Handling" Prompt
Task: Replace return-code error handling with exceptions (or vice versa) safely.
Prompt:
You are a refactoring expert with a focus on error handling. The following legacy code uses return codes (e.g., `-1`, `false`, `null`) to signal errors. Refactor it to use exceptions, following the best practices of [language].
Requirements:
- Identify each return-code site and the corresponding exception type.
- Define custom exception classes if needed.
- Handle the caller side: all callers of this function must be updated to catch exceptions instead of checking return codes.
- If updating all callers is not possible, propose a compatibility wrapper that keeps the old signature but uses exceptions internally.
- Show a comparison of before/after for the function and at least one caller.
Here is the code:
[PASTE CODE]
Why it works: Error handling changes are risky. The prompt addresses the full blast radius, not just the function itself.
Use case: A C function returned 0 for success and non-zero error codes for failures. We converted it to C++ exceptions using this prompt. The trickiest part was the "compatibility wrapper" — the prompt suggested a wrapper function that caught exceptions and translated them back to error codes for the existing C callers. That solved the migration without breaking the entire system.
10. The "Parallel Change / Branch by Abstraction" Prompt
Task: Refactor a widely-used API without breaking existing callers.
Prompt:
You are an expert in the "Branch by Abstraction" and "Parallel Change" techniques (from Martin Fowler's refactoring catalog). I want to replace a widely-used function [function name] with a new implementation. The constraint: existing callers must continue to work unchanged during the transition.
Design a 3-phase plan:
1. Introduce an abstraction (e.g., an interface) that both old and new implementations can adapt to.
2. Migrate callers one by one to the abstraction, keeping the old implementation for non-migrated callers.
3. Remove the old implementation once all callers are migrated.
For each phase, show a code sketch. Identify any hidden dependencies like reflection, serialization, or static imports.
Here is the old function signature and one caller example:
[PASTE CODE]
Why it works: This is a strategic refactoring pattern. The prompt forces the AI to think about a phased rollout instead of a big-bang rewrite.
Use case: We replaced a logging utility that had been called from 200 places. The prompt produced a plan: define a Logger interface, add a factory that returns the old logger by default, then flip callers to the new logger via configuration. We deployed the new logger gradually and rolled back in one environment when we noticed a performance regression. Without the abstraction, a full revert would have been painful.
11. The "Document the Code Base" Prompt
Task: Generate concise, useful documentation for a legacy module.
Prompt:
You are a technical writer. Write documentation for the following legacy module. The goal is to help a new developer who has never seen this code.
Structure:
- **Overview** – what this module does in 2–3 sentences.
- **Key Concepts** – any domain-specific terms or tricky invariants.
- **Main Components** – a table listing each class/function, its responsibility, and its public methods.
- **Dependencies** – external libraries, services, or global state this module touches.
- **Known Issues** – code smells or health issues you noticed (be specific; cite line numbers if possible).
- **Suggested Entry Points** – the functions a developer should read first.
Be honest: do not invent features that are not in the code. If you're unsure about something, say "Unclear from code."
Here is the module:
[PASTE CODE]
Why it works: Documentation is often the first step of understanding. The prompt demands a structured output and explicitly asks the model to admit uncertainty, which maintains trust.
Use case: For a legacy Perl script (1200 lines), this prompt produced a markdown file that became the project's main reference. The "Known Issues" section correctly flagged a global regex variable that was being modified in multiple places — a known source of bugs.
12. The "Refactoring Review Game Plan" Prompt
Task: Turn a static code review into an actionable refactoring backlog.
Prompt:
You are a senior reviewer. Below is a code review report for a legacy project (list of findings). Analyze each finding and convert it into a refactoring task.
For each finding, provide:
- A title (e.g., "Extract validation logic from X").
- The refactoring pattern to use (from Fowler's Refactoring).
- The effort (S/M/L) and risk (H/M/L).
- The recommended order if there are dependencies (e.g., "must do after task #2").
- A small code sketch showing the target design.
Group the tasks into:
1. Quick wins (small effort, low risk).
2. Structural changes (large effort, may need characterization tests).
3. Exploratory / needs more research.
Here is the findings list:
[PASTE FINDINGS]
Why it works: LLMs are good at prioritizing. This prompt helps you build a realistic refactoring roadmap rather than a pile of technical debt.
Use case: After a code review of a legacy Symfony app, we fed the 40 findings into this prompt. It grouped them into 10 quick wins, 15 structural changes, and 15 items that needed more investigation. The quick wins were done in two sprints; the structural changes became a dedicated refactoring project.
Common mistakes to avoid
- Asking for "the best" refactoring without context — always provide the code and its surrounding context.
- Applying AI suggestions directly to production — always run tests, run the linter, and do a code review.
- Refactoring for perfection — the goal is to make the code easier to understand and test, not to achieve an ivory-tower design.
- Forgetting the business stakeholder — a refactoring that breaks a contractual API is a bug. Use the parallel change pattern from prompt #10.
Conclusion
Refactoring legacy code is not a "do it once" event — it's an ongoing discipline. The prompts above are a starting point. They help you build a safety net of characterization tests, break dependencies, and introduce modern patterns one step at a time. As you use them, you'll discover your own variations. The best approach is to start with a small module, run the audit prompt (#1), then characterise it with tests (#2), and then apply the relevant refactoring prompt.
Take the time to adapt these prompts to your own codebase and language. The more context you provide, the better the output. And if you're new to refactoring, study the canonical sources: Martin Fowler's "Refactoring: Improving the Design of Existing Code" and Michael Feathers' "Working Effectively with Legacy Code." These books will give you the vocabulary and mental model that make prompts like these effective.
Ready to try? Copy the first prompt, paste your scariest legacy file, and see what comes out. Then write a characterization test before you change a single line.
Comments