On this page
Code refactoring techniques are small, named changes that improve the structure of code without changing what it does. Six of them cover most everyday work: rename, extract variable, extract function, move function, introduce parameter object and replace conditional with polymorphism. They differ in risk, and the risk decides how you apply each one.
This guide ranks the six by risk, shows before-and-after examples and says which ones an AI assistant can safely draft. If changes keep breaking things nobody touched, the codebase needs a review before more refactoring, and our code audit services give you a ranked fix list to work from.
What is code refactoring?
Code refactoring is changing the internal structure of software so it is easier to understand and cheaper to modify, without changing its observable behaviour. That is Martin Fowler's definition in his book Refactoring (2nd edition, Addison-Wesley, 2018), and its last clause is the one that matters.
The definition rules out three things people often call refactoring. Fixing a bug changes behaviour. Rewriting a module replaces it. Tuning performance changes how the code runs. Each can be worth doing, but each needs its own tests and its own commit.
Fowler describes the work as a series of small behaviour-preserving steps, with the system kept fully working after each one. That is the discipline that makes it safe. A team that takes one small step, runs the tests and commits can stop at any point with working software.
Inherited code with no tests needs an earlier step. Our guide to legacy code refactoring covers how to pin current behaviour before you change anything.
Six code refactoring techniques, ranked by risk
The six techniques below run from lowest to highest risk. Risk here means how many places one change touches and whether an editor can make the change for you. The names follow the catalogue in Fowler's Refactoring. The ranking, the safety nets and the AI guidance are ours.
| Technique | Fixes | Safety net | Delegate to AI? |
|---|---|---|---|
| Rename | Unclear names | The editor's rename command | Use the editor instead |
| Extract variable | Dense expressions | The editor's extract command | Yes |
| Extract function | Long functions | Editor command and unit tests | Yes, then read the diff |
| Move function | Code in the wrong module | Type checker and tests on callers | Yes, one function at a time |
| Introduce parameter object | Long parameter lists | Tests on every caller | Only with tests in place |
| Replace conditional with polymorphism | Repeated type checks | Tests that pin behaviour first | Only with tests and review |
Editors can do the first three mechanically. Extract variable and extract function stay inside one function, and a rename is one command even when the name is used across many files. Visual Studio Code's refactoring documentation lists Extract Method, Extract Variable and Rename Symbol among its built-in operations, supplied by a language service that understands the code. A tool that works on the code's structure is safer than search and replace, and safer than retyping.
The last three cross boundaries. Moving a function changes every file that imports it. A parameter object changes every caller's arguments. Replacing a conditional with polymorphism changes where decisions are made, which is why it needs tests that pin the current behaviour before anyone starts.
Code refactoring examples: before and after
The three code refactoring examples below are invented and written in JavaScript. Each shows one technique, and in each the code gives the same result for valid input before and after.
Extract function
A function that calculates and prints is doing two jobs. Extracting the calculation gives it a name and makes it testable by itself.
// before
function printInvoice(invoice) {
let total = 0;
for (const line of invoice.lines) {
total += line.price * line.quantity;
}
console.log(`Customer: ${invoice.customer}`);
console.log(`Total: ${total}`);
}
// after
function printInvoice(invoice) {
console.log(`Customer: ${invoice.customer}`);
console.log(`Total: ${invoiceTotal(invoice)}`);
}
function invoiceTotal(invoice) {
let total = 0;
for (const line of invoice.lines) {
total += line.price * line.quantity;
}
return total;
}
Introduce parameter object
Two arguments that always travel together are one idea. Grouping them shortens the signature, and it changes every call site, so every caller needs a test.
// before
function bookingsBetween(bookings, startDate, endDate) {
return bookings.filter((b) => b.date >= startDate && b.date <= endDate);
}
// after: callers now pass { start, end }
function bookingsBetween(bookings, range) {
return bookings.filter((b) => b.date >= range.start && b.date <= range.end);
}
Replace conditional with polymorphism
A switch on type tends to be copied wherever that type matters. Giving each type its own class puts the rule in one place.
// before
function monthlyPrice(plan, seats) {
switch (plan.type) {
case "flat":
return plan.base;
case "per-seat":
return plan.base + seats * plan.seatPrice;
default:
throw new Error(`Unknown plan: ${plan.type}`);
}
}
// after
class FlatPlan {
constructor(base) { this.base = base; }
monthlyPrice() { return this.base; }
}
class PerSeatPlan {
constructor(base, seatPrice) { this.base = base; this.seatPrice = seatPrice; }
monthlyPrice(seats) { return this.base + seats * this.seatPrice; }
}
Look at what happened to the error for an unknown plan. It no longer lives in the pricing code. It has to move to wherever plan objects are created, and if nobody moves it, behaviour has changed. This is the kind of detail a test catches and a reviewer misses.
Code refactoring best practices
Five code refactoring best practices keep the work safe: have tests first, take small steps, keep refactoring commits separate, let the editor make the change and stop when a test fails.
- Tests first. Refactoring promises the same behaviour, and only a test can check that promise. If the code has none, write tests around the part you are about to change.
- Small steps. Apply one technique, run the tests, commit. A step that fails is then a few lines to undo, not an afternoon.
- Separate commits. Never mix a refactoring with a feature or a bug fix. A reviewer should be able to read a refactoring commit knowing nothing is meant to behave differently.
- Editor before hands. Where the editor offers the refactoring as a command, use it. It updates every reference, which a person or a text search can miss.
- Stop on red. A failing test after a refactoring means behaviour changed. Undo the step and take a smaller one.
Choose where to refactor by what you are about to change. Cleaning the function you need to edit this week pays back at once. Cleaning code nobody touches does not, and our guide on how to reduce technical debt covers how to rank the rest.
AI code refactoring: what to delegate
AI assistants can draft most refactorings, but they retype code where an editor command transforms it, so behaviour can change without anyone noticing. Delegate the low-risk techniques freely, and the high-risk ones only with tests in place.
Two published studies explain the caution. GitClear's 2025 code quality research, the vendor's own analysis of 211 million changed lines written between January 2020 and December 2024, found that the share of changed lines associated with refactoring fell from 25% in 2021 to less than 10% in 2024. Lines classified as copied and pasted rose from 8.3% to 12.3% over the same period. GitClear links the trend to AI assistants. Its data shows the two moving together and does not prove one caused the other.
Google's research program DORA reached a related finding in its 2024 Accelerate State of DevOps report. AI adoption raised individual productivity, and it also had a negative effect on software delivery stability and throughput. DORA's reading is that fundamentals such as small batch sizes and testing still matter.
In practice, that means a few rules for AI-assisted work:
- Ask for one technique on one function per request, and name the technique.
- Never combine "refactor" with "improve" or "fix" in the same request.
- Run the tests before and after, and read the whole diff.
- For rename and extract, use the editor command. It cannot retype a line wrongly.
The same rules answer a common question about front ends. The best way to get AI to completely refactor frontend code is not to ask for it in one pass. Work one component at a time, with a test or a screenshot comparison for each, and merge each component separately.
If an AI tool wrote most of the product, refactoring is rarely the first job. Our AI code audit checks access control, data exposure, secrets and dependencies first, and the vibe coding risk check gives a quick first read.
When to stop refactoring and audit first
Stop and get a review when refactoring is no longer making changes safer. Four signs say the problem is larger than any of these code refactoring techniques can solve:
- The code you need to change has no tests, and nobody can say what it is supposed to do.
- Each refactoring breaks something in a part of the system that looked unrelated.
- The plan has grown into "refactor everything", with no order and no end.
- Features take twice as long as they used to, and nobody has measured why.
In each case the missing piece is a map. An audit shows which modules carry the risk, which can be left alone and which are cheaper to replace than to repair. Refactoring then follows a ranked list, not a general wish for cleaner code.
How we approach refactoring work
We start with an audit, not a rewrite. Scanners run first, then senior engineers read the code that matters most: sign-in, payments, data access and the paths your users hit every day. You get the findings in plain language, ranked by what to fix first.
From there we can refactor alongside your team or hand the list to your developers. Every engagement is under an NDA, and the IP is always the customer's. See what a source code audit covers.
Frequently asked questions
What are code refactoring techniques?
They are small, named changes that improve the structure of code while its behaviour stays the same. Common ones are rename, extract variable, extract function, move function, introduce parameter object and replace conditional with polymorphism. Each fixes a specific problem, such as an unclear name, a long function or a repeated type check.
Which refactoring technique should I start with?
Start with rename and extract function. Both stay inside a small area of code, and most editors offer them as commands that update every reference for you. They make the code easier to read at once, and they carry the least risk of changing behaviour, which makes them a safe way to build the habit.
What are code refactoring best practices?
Have tests before you start, take one small step at a time, and commit after each step. Keep refactoring commits separate from features and bug fixes. Use the editor's refactoring commands where they exist. If a test fails after a step, undo it and take a smaller one.
Does refactoring change what the code does?
No. By definition, refactoring leaves observable behaviour unchanged and only improves internal structure. If behaviour changes, the change is a bug fix, a feature or a mistake. That is why refactoring depends on tests: they are the only practical way to confirm the code still does what it did before the change.
Can AI refactor code safely?
It can, with limits. AI assistants retype code, so a refactoring can quietly change behaviour. Ask for one named technique on one function, run the tests before and after, and read the full diff. For rename and extract, the editor's own command is safer because it transforms the code without retyping it.
What is the best way to get AI to refactor frontend code?
Work one component at a time. Give the assistant a single component and a named technique, compare the result with a test or a screenshot, and merge it before moving on. Asking for a complete refactor in one pass produces a diff too large to review, so mistakes get through.
How do I know when to stop refactoring?
Stop when the code you are about to change is clear enough to change safely. Refactoring serves the next feature or fix, so cleaning code nobody plans to touch has little return. If every step breaks something elsewhere, stop for a different reason: the codebase needs an audit first.
