# Code Refactoring Techniques That Actually Ship Clean

URL: https://desktoptools.net/journal/code-refactoring-techniques
Type: blog
Locale: en
Published: 2026-06-29
Updated: 2026-06-30

---

> Six code refactoring techniques ranked by blast radius, with the test-first rule that makes all of them safe to run on a Monday morning.

Messy code ships. Clean code also ships. The difference is what happens six months later when the requirements change and nobody wants to touch the module you wrote.

Code refactoring techniques are not about aesthetics. They are about the time you get back, or do not, when a function that was supposed to take 20 lines has quietly grown to 300 and nobody remembers why.

Here is what I have actually used over four years of Rust and Go backend work, ordered by how disruptive they are to a running codebase. Low blast radius first.

## The One Rule Before You Start

Refactor only code that has tests. Not "probably tested somewhere," not "the integration suite hits it." Green unit tests that run in under 30 seconds and tell you exactly what broke.

This is not a suggestion. It is the only thing separating a productive refactoring session from a two-day debugging session that reintroduces the original bug in a different location. If the code has no tests, write them first. That is also refactoring work. It is just the kind nobody calls refactoring.

One more rule: version control with atomic commits. Every technique below should be one commit. The message says what technique you applied, not "cleanup." If something breaks on staging, you roll back one commit, not three.

![Clean structured code with organized functions and modules on a laptop screen](https://fdzlnqpwsaniezitwiuw.supabase.co/storage/v1/object/public/cms-media/desktoptools/2026-06/df7699-inline2.webp)

## Extract Method: The One You Use Every Day

Extract Method is what you reach for first. You have a function doing three things. You pull out one of them into its own function with a name that says what it does.

The trigger: any method longer than you can read on one screen without scrolling. The practical threshold I use is 40 lines. Beyond that, something can be named and extracted.

The mechanics:

`// Before: one function doing three unrelated things
func processOrder(order Order) error {
    // validate fields (15 lines)
    // calculate totals (20 lines)
    // persist to database (12 lines)
    return nil
}

// After: each concern is named and testable
func processOrder(order Order) error {
    if err := validateOrder(order); err != nil {
        return err
    }
    total := calculateOrderTotal(order)
    return persistOrder(order, total)
}`Extract Method also does something most developers miss: it forces you to name things. If you cannot name the extracted function without using "and" or "or," it is still doing too much.

Skip if: the function is genuinely simple and extraction would scatter logic across three files for no gain. Not every 30-line function needs surgery. The test is whether the extracted piece has a meaningful name on its own.

In practice, I run Extract Method at least twice a week. It is the lowest-risk, highest-payoff technique on this list. An IDE does it in two keystrokes. The resulting code is smaller, each piece is individually testable, and the next developer reading it does not need to hold the whole function in their head at once.

## Rename Variable: Free Cognitive Load Reduction

This costs nothing and pays back immediately. A well-named variable eliminates the need for a comment explaining what it holds. A variable called `d` in a function that runs for 200 lines is technical debt measured in minutes per future reader.

The rename: `d` becomes `invoiceDueDate`. Done.

Modern IDEs handle this in one keystroke: F2 in VS Code, Shift+F6 in IntelliJ. The refactoring is instant. The compounding benefit persists for the lifetime of the codebase.

Where this matters most: code that crosses module boundaries, code that other developers touch, code you will read six months from now in a pull request review under time pressure. The cognitive overhead of decoding abbreviated names is real and cumulative.

A naming rule I keep: if the variable name describes its type rather than its role, rename it. `string` becomes `customerEmail`. `list` becomes `pendingInvoices`. `data` becomes whatever the data actually represents.

## Replace Temp with Query

You have a temporary variable that holds the result of an expression. The variable is used once or twice. Replace it with a method call.

`// Before
basePrice := item.Quantity * item.UnitPrice
if basePrice > 1000 {
    return basePrice * 0.95
}
return basePrice

// After
if basePrice() > 1000 {
    return basePrice() * 0.95
}
return basePrice()

func (item Item) basePrice() float64 {
    return item.Quantity * item.UnitPrice
}`The gain: the calculation is now testable in isolation and reusable across any method that needs it. The cost: one extra function call. In any language with a compiler or JIT, this cost is zero in production. If you are writing performance-critical hot paths, profiler-verified ones, skip this technique there and document why.

I use this most in domain logic where the same derived value appears across multiple methods. Three functions all recalculating `basePrice` differently is a bug waiting to happen when the pricing formula changes. One method means one place to update.

The secondary benefit is readability in conditions. `if basePrice() > discountThreshold()` reads as an intention, not a computation. You understand the condition without parsing the arithmetic.

![Developer reviewing passing tests in terminal after a refactoring session](https://fdzlnqpwsaniezitwiuw.supabase.co/storage/v1/object/public/cms-media/desktoptools/2026-06/9e9ad7-inline3.webp)

## Replace Conditional with Polymorphism

You have a switch statement or a chain of if/else blocks branching on object type. Each branch does something different for each type. This is the pattern that signals a missing abstraction.

The fix: create a base class or interface that handles the common contract. Each type gets its own implementation.

`// Before: type switch with growing branches
func calculateShipping(order Order) float64 {
    switch order.ShippingType {
    case "standard":
        return order.Weight * 0.5
    case "express":
        return order.Weight * 1.2
    case "overnight":
        return order.Weight * 2.5
    }
    return 0
}

// After: each type owns its behavior
type ShippingCalculator interface {
    Calculate(weight float64) float64
}

type StandardShipping struct{}
func (s StandardShipping) Calculate(weight float64) float64 { return weight * 0.5 }

type ExpressShipping struct{}
func (e ExpressShipping) Calculate(weight float64) float64 { return weight * 1.2 }

type OvernightShipping struct{}
func (o OvernightShipping) Calculate(weight float64) float64 { return weight * 2.5 }`Adding a new shipping type now means adding a new struct. It does not mean touching the existing switch and risking a regression in the `standard` case. The Open/Closed Principle, in practice.

This technique has a higher blast radius than Extract Method. It reorganizes responsibilities between types. Apply it when the switch statement has more than three branches and when new branches are being added regularly. If it is a stable two-case condition, a simple if/else is fine. Do not reach for polymorphism because it sounds more sophisticated.

The tell that this refactoring is warranted: you just added a fourth branch and noticed you also had to scroll through the function to find where the third one was. The cognitive cost of reading that switch grows linearly with the number of cases. An interface grows by one file.

## Introduce Parameter Object

You have a method that takes six parameters. Three of them always appear together and relate to the same concept. Group the three into a struct.

`// Before
func createInvoice(
    customerId string,
    dueDate time.Time,
    currency string,
    lines []InvoiceLine,
) Invoice

// After
type InvoiceParams struct {
    CustomerID string
    DueDate    time.Time
    Currency   string
}

func createInvoice(params InvoiceParams, lines []InvoiceLine) Invoice`The practical benefit is not just cleaner call sites. It is that `InvoiceParams` becomes a stable type you can validate, log, and pass around. You can add a `Region` field later without changing the signature of every function that calls `createInvoice`. The call sites pass a struct, not a positional argument list where swapping `dueDate` and `customerId` compiles fine and is wrong.

Skip this when the parameters are genuinely unrelated and grouping them would create an artificial object with no real cohesion. A struct named `MiscParams` is not an improvement.

I reach for this technique when I see tests constructing the same parameter combination more than three times. That repetition signals that the combination has a name worth assigning.

## Substitute Algorithm

The existing implementation works. It is also wrong, not logically, but architecturally. You learned a better way to structure the computation. You want to replace the whole thing, not patch it.

This is the highest blast radius refactoring on this list. You are not adjusting a method. You are replacing how a problem is solved.

The prerequisite: a complete test suite that describes what the algorithm must do, independent of how it does it. You run the old implementation against the tests: green. You write the new implementation. You run the tests again: green. You delete the old code.

I used this once last year, migrating a hand-rolled pagination cursor implementation to a keyset-based approach. The tests caught two edge cases the old implementation had accidentally correct behavior for. Without the tests, the migration would have shipped with two silent regressions that only surfaced under load.

Substitute Algorithm is not for Tuesday afternoon. It is for when you have a clear design reason, full test coverage, and a timeboxed window with staging verification before the next deploy.

![Two developers collaborating on a code review session at a shared monitor](https://fdzlnqpwsaniezitwiuw.supabase.co/storage/v1/object/public/cms-media/desktoptools/2026-06/8a9548-inline4.webp)

## On AI-Assisted Refactoring

The tooling is genuinely useful for Extract Method and Rename Variable at scale: finding every call site, generating the replacement, opening a PR across multiple files. [Sourcegraph Batch Changes](https://sourcegraph.com/batch-changes) and OpenRewrite for JVM projects do this across a repo of any size without the human-error rate of manual search-and-replace.

The honest picture on AI and code quality: a 2025 study from the [METR alignment research group](https://metr.org) found experienced developers using AI assistance were measurably slower on tasks involving fundamental structural changes. The tooling improved output on mechanical tasks and hurt it on design-level tasks. That finding is consistent with my experience.

For Replace Conditional with Polymorphism or Substitute Algorithm, the design judgment is yours. The AI can generate the boilerplate once you know what you want. That division of labor works. Do not reverse it and expect the tool to decide when polymorphism is the right abstraction.

What AI does well in this context: it can spot candidate methods for extraction by surfacing high cyclomatic complexity, flag variables named with single letters, and identify duplicated expressions across files. SonarQube and CodeScene do this without the hallucination risk. Use them in your CI pipeline to surface where to look, not to decide what to do.

## When Refactoring Is the Wrong Move

Not every messy function deserves cleanup this week.

If the code is scheduled to be deleted in the next quarter, skip the refactoring. If there are no tests and no time to write them, document the debt and move on. If you are the only person who will ever touch this module, the cost-benefit calculus changes.

Refactoring is a bet on future change. The bet pays off when the code is touched again. Legacy code that has been stable for three years and will remain stable is not a problem to solve. It is a stable system doing its job.

The code that deserves the most attention is the code that is changing. Your most active modules, your most-touched files, your most-reviewed functions. That is where clarity compounds the fastest.

## The Order Matters

Start with Rename Variable and Extract Method. They are safe, fast, and compound over time. Move to Replace Temp with Query and Introduce Parameter Object when the codebase is stable and tested. Use Replace Conditional with Polymorphism and Substitute Algorithm only when test coverage is solid and the design problem is clear.

Six techniques. One rule: tests first, always. No shortcuts on that part.

## FAQ

### What is the safest code refactoring technique to start with?

Rename Variable and Extract Method are the lowest-risk techniques. They do not change behavior: they only reorganize how code is expressed. Any modern IDE handles both in a single keyboard shortcut with automatic reference updates across the codebase.

### Do you need tests before refactoring?

Yes. Refactoring without tests means you cannot verify that behavior is preserved. The standard approach: write tests against the current behavior first, then refactor until the tests still pass. If you cannot write tests because the code is too tightly coupled, untangling that coupling is the first refactoring task.

### What is the difference between refactoring and rewriting?

Refactoring changes the structure of code without changing its behavior: each step is small and verifiable. Rewriting replaces a system with a new implementation, often in parallel. Refactoring is lower risk because you can stop at any step and ship. Rewriting requires completing the whole migration before deploying.

### When should you use Replace Conditional with Polymorphism?

When a switch statement or if/else chain branches on object type and you expect new types to be added over time. If the branching logic is stable with two or three cases, a simple conditional is fine. The polymorphism approach pays off when adding a new type should not require touching existing code paths.

### Can AI tools handle code refactoring?

AI tools are effective for mechanical refactoring at scale: finding all call sites, applying consistent renames, generating boilerplate for extracted methods across large repos. They are less reliable for design-level refactoring like Replace Conditional with Polymorphism, where understanding why the current design is wrong requires judgment the current tools do not reliably supply.

### What is the Substitute Algorithm technique?

Substitute Algorithm replaces an entire implementation with a better approach while keeping the external behavior identical. The prerequisite is a complete test suite that describes expected behavior independently of implementation. You run tests against the old code, write the new implementation, run tests again. If they pass, delete the old code.

### How do you know when code needs refactoring?

Common signals: methods longer than 40 lines, functions with more than five parameters, switch statements with growing branches, variables named with single letters or abbreviations, and the need to read a method twice to understand what it does. The practical test: if adding a feature requires understanding more than two modules in detail, the abstraction boundaries are probably wrong.