Prompt: Build a Refactor Plan
A prompt for a staged, low-risk refactor plan that separates behaviour-preserving cleanup from riskier structural changes.
You are a senior engineer planning a refactor before writing any code.
Objective: Build a staged refactor plan for the code below, written in {{LANGUAGE}}, with the goal: {{REFACTOR GOAL — e.g. reduce duplication, improve testability, split a large function}}.
Context: Current test coverage: {{TEST COVERAGE — e.g. none, partial, full}}. Constraints on the refactor: {{CONSTRAINTS — e.g. can't change the public API, must ship incrementally}}.
Constraints:
- Split the plan into stages ordered from lowest to highest risk; each stage must be independently shippable
- For each stage, state what changes and what must NOT change (behaviour preservation)
- If test coverage is not "full", recommend which tests to add before the riskiest stage, not after
- Flag any stage that would require a change to the public API given the constraints
Output format: Return a numbered list of stages, each with "Changes", "Must not change", and where relevant "Tests to add first".
Before finishing, check that no stage depends on a later stage being completed first.
Code:
```
{{PASTE CODE HERE}}
```
Example
Input
LANGUAGE: TypeScript
REFACTOR GOAL — e.g. reduce duplication, improve testability, split a large function: Split a large function that handles validation, calculation and formatting into separate concerns
TEST COVERAGE — e.g. none, partial, full: partial — a few integration tests exist, no unit tests
CONSTRAINTS — e.g. can't change the public API, must ship incrementally: Can't change the public API of the exported function
PASTE CODE HERE:
export function processOrder(order) {
if (!order.items.length) throw new Error("empty order");
let total = 0;
for (const item of order.items) total += item.price * item.qty;
if (order.discountCode) total *= 0.9;
return `Order total: £${total.toFixed(2)}`;
}
Output
1. Add unit tests around current behaviour. Changes: none to the code — add unit tests covering empty order (throws), single item, multiple items, and discount code applied. Must not change: existing behaviour; these tests should pass against the current code unmodified. 2. Extract validation into its own function. Changes: move the empty-order check into a separate `validateOrder(order)` function called at the top of `processOrder`. Must not change: the thrown error message or the public `processOrder` signature. Tests to add first: none beyond stage 1 — the existing empty-order test should still pass unchanged.
Show more
3. Extract formatting into its own function.
Changes: move the `£${total.toFixed(2)}` string formatting into `formatOrderTotal(total)`.
Must not change: the output string format, since external callers may depend on it.
4. Extract calculation into its own function (highest risk, as it touches the pricing logic).
Changes: move the total and discount calculation into `calculateTotal(order)`, returning a number.
Must not change: the discount logic (10% multiplier) or the unrounded total passed on to formatting.
Tests to add first: a unit test asserting `calculateTotal` returns the correct number for the stage 1 cases before wiring it in.
No stage changes the exported `processOrder` signature, so the public API constraint is respected throughout.
Why it works
- Ordering stages from lowest to highest risk means an interrupted refactor still leaves the codebase in a working, shippable state.
- Requiring 'must not change' per stage makes behaviour preservation explicit, not assumed, which is the actual definition of a safe refactor.
- Front-loading test additions before the riskiest stage catches regressions before they ship, instead of discovering them after the fact.
Why the basic version fails
“Refactor this function” usually produces a single rewritten block that changes everything at once — structure, naming, and sometimes behaviour — with no way to review or ship it incrementally, and no clear statement of what was meant to stay the same versus what was meant to change.
What this prompt fixes
Ordering stages from lowest to highest risk means the refactor can be shipped and reviewed incrementally, and an interrupted refactor still leaves a working codebase rather than a half-finished rewrite. Requiring an explicit “must not change” per stage turns behaviour preservation into a stated commitment rather than an assumption the reviewer has to verify by reading a large diff. Front-loading test additions before the riskiest stage, rather than after, catches regressions before they ship.
Tips per model
- Claude tends to order stages sensibly by risk and keeps each one genuinely independent, which matters for reviewability.
- ChatGPT is fast at spotting the natural seams to split a large function along, useful as a first pass before refining the plan.
- State the real test coverage honestly — a plan for “full coverage” when coverage is actually partial will skip test recommendations you need.