Structural change with the behaviour held still, in reviewable steps. Below are 7 copy-ready prompts. Fill in the [BRACKETS], copy, and paste into ChatGPT, Claude, Gemini or any capable assistant.
Refactoring with AI goes wrong in one specific way: you ask it to tidy something and it quietly changes what the code does, because the tidier version is the one it has seen more often.
The 7 prompts
Refactor safely without changing behaviour
Restructure code while keeping it provably equivalent.
Refactor this code. Behaviour must not change. CODE: ``` [PASTE] ``` LANGUAGE: [DETAIL] WHAT BOTHERS ME ABOUT IT: [YOUR REASON FOR REFACTORING] TEST COVERAGE: [good / partial / none] WHO CALLS THIS: [IF KNOWN] Produce: 1. BEHAVIOUR INVENTORY - before refactoring, catalogue everything this code currently does, including the things it probably does by accident: return values in each path, exceptions thrown, side effects, mutation of inputs, order of operations where observable, and behaviour on edge inputs. 2. THE ACCIDENTAL BEHAVIOUR - which of the above is likely unintentional but might be depended on. This is the trap in every refactor. Flag each. 3. IF COVERAGE IS PARTIAL OR NONE - the characterisation tests to write FIRST, capturing current behaviour exactly as it is, bugs included. Do not proceed past this until they exist. 4. THE REFACTOR - in small, individually safe steps. Each step: what changes, why it is behaviour-preserving, and how to verify. Never combine a refactor step with a behaviour change. 5. THE REFACTORED CODE. 6. EQUIVALENCE ARGUMENT - for each non-trivial change, why the new version is equivalent. Where equivalence depends on an assumption (inputs are never null, the list is never empty), state the assumption explicitly. 7. WHAT I WOULD FIX BUT DID NOT - actual bugs noticed during the refactor. List them separately as a follow-up. Do not fix them here; mixing a bug fix into a refactor makes both unreviewable. Rules: - Do not add features, do not fix bugs, do not change the public interface unless I asked - If the code cannot be safely refactored without tests, say so and stop at section 3
What you get: A behaviour inventory, characterisation tests where needed, stepwise refactoring with equivalence arguments, and bugs listed separately.
Tip: Section 7 is the discipline. Fixing a bug during a refactor makes it impossible to tell which change broke production.
Break up a function that has grown too large
Decompose a 300-line function into something maintainable.
Break up this function. CODE: ``` [PASTE] ``` LANGUAGE: [DETAIL] WHY IT NEEDS BREAKING UP: [hard to test / hard to read / too many responsibilities / merge conflicts] Produce: 1. RESPONSIBILITY MAP - read through and mark the distinct things this function does. Give the line ranges. Name each responsibility as a verb phrase. 2. THE SEAMS - the natural split points. A good seam has few variables crossing it. For each candidate split: what data crosses the boundary in each direction. Rank the seams by how little crosses them - that is the best split. 3. THE HARD PARTS - variables mutated across sections, early returns that skip later code, shared error handling, a loop whose body spans several responsibilities. These resist splitting; say how to handle each. 4. THE DECOMPOSITION - the new functions, with signatures. For each: name, parameters, return, and whether it is pure. Aim for as many pure functions as possible - they are the testable ones. 5. THE CODE - refactored, in steps, each step compilable and behaviour-preserving. 6. WHAT GOT BETTER AND WHAT GOT WORSE - be honest. Decomposition costs indirection. If a section is better left long, say so. Three functions used once each and passing eight parameters around is worse than one clear function. 7. NOW TESTABLE - what can be unit-tested after this that could not before. If the answer is 'nothing', question whether the refactor is worth it. Rules: - Do not extract a function just because of line count - Naming is the deliverable. A function named processData has not been decomposed, only moved. - Prefer pure functions over functions taking many parameters and mutating them
What you get: A responsibility map, ranked seams, a decomposition with named pure functions, and an honest cost assessment.
Tip: Section 2's 'rank seams by how little data crosses' is the actual technique. Splitting where seven variables cross the boundary produces worse code than leaving it alone.
Plan a migration from one library or framework to another
Get from technology A to technology B without a big-bang rewrite.
Plan a migration. FROM: [CURRENT TECHNOLOGY AND VERSION] TO: [TARGET] WHY: [THE REASON - be honest, including if it is partly preference] CODEBASE SIZE: [ROUGH LINES, NUMBER OF FILES/MODULES AFFECTED] TEAM: [SIZE AND FAMILIARITY WITH THE TARGET] CAN WE PAUSE FEATURE WORK: [yes / no / partially] CURRENT CODE SAMPLE: ``` [PASTE REPRESENTATIVE CODE] ``` Produce: 1. IS THIS WORTH IT - given my stated reason, an honest assessment. What does the target actually buy, what does it cost, and is there a cheaper way to get the same benefit? If the reason is mostly preference, say so. 2. CONCEPT MAPPING - a table: concept in source | equivalent in target | how clean the mapping is (direct / awkward / no equivalent). The 'no equivalent' rows are where the migration will actually hurt. 3. INCREMENTAL PATH - can both coexist? If yes, the strangler pattern for this specific pair: what the boundary is, how to route between them, and the order to migrate modules. If no, say so plainly - that changes the whole plan. 4. ORDER OF MIGRATION - which modules first. Prefer: low risk, high learning, few dependents. Name the specific first candidate and why. 5. THE POINT OF NO RETURN - the step after which abandoning the migration is expensive. Identify it and say what should be proven before crossing it. 6. WHAT WILL GO WRONG - specific to this pair. Common ones: subtle behavioural differences, different defaults, error handling semantics, ecosystem gaps, performance surprises, type system differences. 7. EFFORT - as a range per phase, with the largest uncertainty named. 8. THE ABANDON CRITERIA - written now: what we would have to learn to stop this. Do not assume the migration is a good idea. Answer section 1 honestly first.
What you get: An honest worth-it assessment, a concept mapping with the painful gaps identified, an incremental path and pre-written abandon criteria.
Tip: Section 8 written at the start is the only protection against a migration that is 60% done forever. Nobody can define it once two quarters are invested.
Reduce duplication without over-abstracting
Decide what to unify and what to leave duplicated.
I have duplicated code. Help me decide what to do about it - the answer may be nothing. THE DUPLICATED CODE: ``` [PASTE THE INSTANCES, SEPARATED BY ---] ``` LANGUAGE: [DETAIL] WHERE EACH LIVES: [MODULE/CONTEXT FOR EACH] HOW THEY CAME TO EXIST: [copy-paste / independent development / unknown] HAVE THEY DIVERGED: [DESCRIBE ANY DIFFERENCES] Produce: 1. SAME CODE OR SAME SHAPE - the critical question. Two pieces of code can look identical while representing different concepts that will evolve apart. Assess: do these represent the same rule, or two rules that currently coincide? Look at the contexts I gave you. 2. THE DIVERGENCE TEST - for each realistic future change, would all instances need to change together, or independently? Work through at least three plausible changes. If they would change independently, do not unify them. 3. RECOMMENDATION - one of: - UNIFY: same concept, will change together. Show the abstraction. - LEAVE: different concepts that coincide. Say so and explain why unifying would be worse. Suggest a comment noting the deliberate duplication. - PARTIAL: extract the genuinely shared part, leave the rest. Usually the right answer. Show the split. 4. IF UNIFYING - the abstraction, with: where it should live, its interface, and how each call site changes. Warn if the abstraction needs a boolean or mode parameter to serve both cases - that is a strong sign these are two things, not one. 5. THE COUPLING COST - unifying creates a dependency between these modules. Is that acceptable given where they live? Shared code across bounded contexts is usually worse than duplication. Be willing to recommend leaving duplication in place. Premature abstraction is more expensive to undo than duplication.
What you get: A same-concept-or-coincidence judgement, a divergence test over realistic changes, and a recommendation that may be to leave it alone.
Tip: The boolean-parameter warning in section 4 is the best available signal. If your unified function needs a mode flag, you have merged two things that were not one.
Improve code that works but is hard to change
Make future changes cheaper without breaking anything.
This code works but is painful to modify. Diagnose why and fix it. CODE: ``` [PASTE] ``` RECENT CHANGES I HAD TO MAKE TO IT: [WHAT YOU CHANGED AND WHAT IT COST] CHANGES I EXPECT TO MAKE: [WHAT IS COMING] LANGUAGE: [DETAIL] Produce: 1. WHY IT RESISTS CHANGE - diagnose specifically, not generically. Look for: shotgun surgery (one change touches many places), a feature envy pattern, hidden temporal coupling (A must be called before B with nothing enforcing it), primitive obsession, conditionals that grow every time a case is added, configuration scattered through logic, and business rules embedded in the wrong layer. 2. THE CHANGE COST MAP - for my expected upcoming changes, which parts of this code each one touches. Where one change touches many places, that is the thing to fix. 3. WHAT TO CHANGE - aimed specifically at making my expected changes cheap. Not general cleanliness. If a piece of ugly code is stable and nothing upcoming touches it, leave it and say so. 4. THE REFACTORED CODE - in safe, ordered steps. 5. THE TEST-THIS-FIRST LIST - what needs test cover before starting. 6. WHAT IS STILL UGLY AND WHY THAT IS FINE - the parts not worth fixing. Justify each. Rules: - Optimise for my stated upcoming changes, not for abstract quality - Do not introduce a design pattern by name without saying what concrete problem it solves here - Do not add extension points for changes I have not said are coming. Speculative generality is the most common way this goes wrong.
What you get: A specific diagnosis of change-resistance, a change-cost map against your real roadmap, and a justified list of what to leave alone.
Tip: Anchoring on your actual upcoming changes is what keeps this from becoming a pattern-application exercise. Code that never changes does not need to be elegant.
Modernise old code to current language features
Bring an old codebase up to the current idioms of its language.
Modernise this code to current [LANGUAGE] idioms. CURRENT LANGUAGE VERSION: [OLD VERSION] TARGET LANGUAGE VERSION: [NEW VERSION] CODE: ``` [PASTE] ``` CONSTRAINTS: [MINIMUM SUPPORTED VERSION, DEPENDENCIES THAT LIMIT US] Produce: 1. AVAILABLE IMPROVEMENTS - a table: Current pattern | Modern equivalent | Version required | Benefit | Risk of changing Cover the relevant ones for this language: newer syntax, standard library additions that replace hand-rolled code, better error handling constructs, type system features, async improvements, immutability and pattern matching features. 2. SAFE / CAREFUL / RISKY - group the changes: - SAFE: purely syntactic, provably equivalent - CAREFUL: semantically equivalent in normal cases but differs at edges. Name the edges. - RISKY: changes behaviour in ways that might be depended on 3. THE SEMANTIC TRAPS - modernisations that look equivalent but are not. Every language has these: differences in null/undefined handling, shallow vs deep copy, evaluation order, iteration over a collection being mutated, integer division, string comparison. Name the ones relevant to this code. 4. THE MODERNISED CODE - safe changes applied. Careful changes shown separately with the edge case noted inline. 5. LEAVE ALONE - old-style code that is fine. Modernising working code carries risk and no benefit unless it improves clarity or correctness. Be selective. 6. WHAT THE OLD CODE DID THAT THE NEW FEATURE DOES NOT - occasionally the old verbose version handled a case the concise version does not. Check for this. Do not modernise for its own sake. Each change needs a benefit beyond 'this is how it is done now'.
What you get: Risk-grouped modernisations with semantic traps named and a leave-alone list.
Tip: Section 3 is what prevents a modernisation from introducing bugs. Most 'equivalent' syntax changes differ somewhere at the edges.
Extract a module or service from a monolith
Pull one piece out of a large codebase cleanly.
Help me extract this from a larger codebase. WHAT I WANT TO EXTRACT: [THE COMPONENT/DOMAIN] WHY: [independent deployment / different scaling / team ownership / reuse] CURRENT CODE: ``` [PASTE THE RELEVANT CODE AND ITS DEPENDENCIES] ``` TARGET: [separate module in the same repo / separate package / separate service] DATABASE SITUATION: [shared database / can be separated / unknown] Produce: 1. BOUNDARY ANALYSIS - what belongs inside and what stays outside. For anything ambiguous, say which side and why. Get this wrong and everything after it is wasted. 2. DEPENDENCY AUDIT: - OUTBOUND: what the extracted code calls. Each becomes an interface or a network call. - INBOUND: what calls the extracted code. Each becomes a client. - SHARED DATA: tables, caches, files touched by both sides. This is the hard part. - SHARED CODE: utilities and types used by both. Duplicate, extract to a shared library, or move - decide for each. 3. THE DATA PROBLEM - if extracting to a service and the database is shared, work through: which tables belong to which side, foreign keys crossing the boundary, transactions spanning both, and queries joining across. For each crossing, the options and their cost. If the data cannot be cleanly split, say so plainly - that may mean not extracting to a service at all. 4. THE INTERFACE - the API between the two sides. Keep it narrow. For each operation: what it does, its data, and whether it can be asynchronous. 5. WHAT YOU LOSE - be explicit: transactional consistency across the boundary, type safety if it becomes a network call, easy refactoring across the seam, a simpler local development setup, and one deploy instead of two. 6. THE STEPS - in order, each independently shippable: a. Enforce the boundary in the existing codebase first (module, package, namespace) with no deployment change b. Then separate the data c. Then extract Do not skip step (a). Most failed extractions skipped it. 7. SHOULD YOU - given my stated reason, is extraction the right answer? A module boundary inside the monolith gets most of the benefit at a fraction of the cost. Say so if that applies.
What you get: A boundary and dependency analysis, the data-splitting problem worked through, an explicit losses list and a staged plan starting inside the monolith.
Tip: Step 6a is the advice that matters. If you cannot enforce the boundary as a module in one codebase, you will not enforce it across a network either.
Where AI actually helps here
- Naming things. It is genuinely better than most of us at this
- Extracting a function and finding every call site in code you pasted
- Suggesting a structure when you know the current one is wrong but not why
Where it falls down
- Large refactors in one pass. Beyond a few hundred lines things silently change
- Preserving behaviour you did not mention — the workaround that looks like a bug and is not
- Knowing your conventions unless you state them
The mistake almost everyone makes: Refactoring without a diff
Ask for the change as a diff, not as the whole rewritten file. A full-file rewrite hides the four lines it altered while you were reading the ninety it did not. And add: if you change any behaviour, stop and tell me instead.
Free tool: Prompt Compare
Runs in your browser. No sign-up, nothing uploaded.
Questions people ask
Is it safe to let AI refactor code?
In small, reviewable steps with tests in place, yes. In one pass over a large file, no — and the risk is not that it breaks loudly, it is that it changes an edge case quietly.
How much code can I paste at once?
Context windows are large, but attention is not uniform across them. In practice quality degrades well before the limit. One file, or one coherent unit, produces better results than a whole module.