Skip to content

R.E.F.A.C.T. refactoring

Recognize smell · Extract intent · Flatten complexity · Add tests first · Change in small steps · Track regressions

R.E.F.A.C.T. is the safe pattern for asking an AI assistant to refactor existing code. The temptation with AI assistance is the opposite: paste a 600-line file, ask for a "clean rewrite," accept what comes back. That pattern is responsible for a meaningful share of post-AI quality regressions.

The discipline R.E.F.A.C.T. enforces is two-fold:

  1. Never refactor without test coverage. The model will confidently produce a "cleaner" version that silently changes behavior. Without tests, the team will not notice until the bug reaches production.
  2. Always work in reversible steps. A 600-line rewrite that fails review has to be discarded entirely. Six 100-line steps can each be accepted, revised, or reverted on their own.

Why this matters now

GitClear's 2023 study measured a near-doubling of code reverted within two weeks between 2020 and 2023, coinciding with the expansion of AI coding assistants1. "Rewrite this file" prompts are a leading contributor to that pattern.

When to use R.E.F.A.C.T.

  • A function or module has been flagged for clarity, duplication, or size in code review.
  • A team is modernizing a legacy module (a Java 8→17 migration, a React class-component-to-hooks migration).
  • A bug fix has revealed a structural problem that should be addressed before the next change.

When not to use it: in the same diff as a behavior change. Refactor or change behavior, not both. A diff that does both is uncheckable because the reviewer cannot tell which delta caused which test to fail.

The six steps

R. Recognize smell

Before involving the model, name the specific smell the refactor addresses:

  • Long function (cyclomatic complexity, lines, parameters).
  • Duplication (same logic in multiple places).
  • Tangled dependencies (a class that knows too much about its callers).
  • Mixed levels of abstraction (HTTP concerns interleaved with domain logic).
  • Unclear naming (function name lies about what the function does).

A refactor without a named smell is a rewrite. Rewrites are a different activity with different risks; do not use R.E.F.A.C.T. for them.

E. Extract intent

Write down, in plain English, what the code is supposed to do. Often this is the missing artifact: code that has drifted from its documented intent, or code that never had documented intent.

The intent statement becomes the contract the refactor preserves. It also becomes the S.C.A.F.F. Specify block when you prompt the model.

If you cannot write the intent down, you cannot refactor; you can only guess. Stop and reverse-engineer the intent first, possibly with the model's help, but as an explicit step, not implicitly.

F. Flatten complexity

With intent in hand, identify the smallest reduction of complexity that addresses the smell:

  • Extract a helper function.
  • Flatten a nested conditional.
  • Replace a switch with a lookup.
  • Pull a class out of a god-object.
  • Replace a deep call chain with a single dispatch.

The right answer is rarely "rewrite the whole file." It is more often "these three lines become this one helper, and that helper now has a test."

This step is where the model is most useful: it can suggest the shape of a refactor faster than a human can. But the human picks the minimum useful refactor, not the maximum impressive one.

A. Add tests first

Before the refactor, the existing behavior must be pinned down by tests. This is the single most-skipped R.E.F.A.C.T. step and the one that prevents the most regressions.

  • For each branch of the function being changed, there is a test.
  • For each error path, there is a test.
  • For each side-effect (DB write, log line, queue publish), there is a test that asserts it.
  • For each input class the function handles, there is a test.

The model can help generate these tests, but the developer reads each one to confirm it tests the actual contract, not what the model guessed the contract was. A test that pins down a bug is still a useful pin: refactor without changing the bug, then fix the bug in a separate diff.

If the function is genuinely untestable as written, that is the refactor: make it testable, in a tiny first step, before any further change.

C. Change in small steps

Each refactoring step is small enough to:

  • Be reviewed in under 15 minutes.
  • Pass the existing test suite without modification.
  • Be reverted independently if it fails review.
  • Make sense on its own as a commit message.

A single S.C.A.F.F. prompt produces a single step. Then VERIFY. Then commit. Then the next step.

The discipline of small steps is what makes AI-assisted refactoring reversible: the property that makes it safe to do at all.

T. Track regressions

After each step:

  • Run the full test suite, not just the local one.
  • Run integration tests if they exist.
  • Where applicable, run performance benchmarks. Refactors that improve readability sometimes cost throughput.
  • Track review velocity and revert rate as team-level signals (see metrics taxonomy).

If a step regresses something, revert the step and re-prompt. Do not "fix forward" through a chain of patches; that is how the original 600-line rewrite happens by stealth.

Worked example: decomposing a 200-line function in five steps

A controller method has grown to 200 lines, mixes HTTP, domain logic, and persistence, and has one happy-path test.

Step 0. Pin down behavior

Add tests for: each conditional branch, each error-response path, each DB call, each emitted event. Five new tests; no production-code changes. Commit.

Step 1. Extract the request validation

Move the input-validation block (lines 1–40) into a validate_request function. The controller calls it. No behavior change. Run all tests. Commit.

Step 2. Extract the persistence layer

Move the DB read/write block into a repository.find_and_update function. The controller calls it. No behavior change. Run all tests. Commit.

Step 3. Extract the event emission

Move the event-emission block into the existing event bus wrapper. Controller delegates. No behavior change. Run all tests. Commit.

Step 4. Extract the response formatting

Move response-shaping into a serializer function. Controller now returns the serializer's output. No behavior change. Run all tests. Commit.

Step 5. Final controller cleanup

Controller is now ~30 lines, calls four helpers in sequence, has clear intent. Naming pass; final tests; commit.

Each step is reviewed independently. Any step that fails review or regresses tests is reverted alone, not the whole train. The team can stop the refactor at any step and ship: every commit between Step 1 and Step 5 is a valid stopping point.

That property, every intermediate commit is a valid stopping point, is what separates R.E.F.A.C.T. from a rewrite.

Sizing guidance

Sizing guide

  • Small (< 50). R.E.F.A.C.T. as written, one developer at a time. Steps may be smaller (50 lines) given lighter review.
  • Mid (50–500). Tie R.E.F.A.C.T. to the team's sprint cadence: one named refactor per sprint, with tests-first as the acceptance criterion.
  • Enterprise (500+). Treat large modernizations (Java 8→17, Python 2→3, framework migrations) as R.E.F.A.C.T. programs with named owners, regression dashboards, and rollback plans. Public reference points: Amazon Q Developer's Java migration2.
  • Regulated. Refactor steps that touch Tier High or Critical systems require dual review and a Decision Record per step, not per program.

Anti-patterns

Don't

  • Don't prompt "rewrite this file" or "clean this up". That is a request for a one-shot rewrite, not a refactor. The output is unreviewable and irreversible.
  • Don't refactor without tests in place first. The model will "improve" code in ways that quietly change behavior; without tests, the team only finds out in production.
  • Don't combine refactor and behavior change in one diff. The reviewer cannot tell which delta caused which failure, and the revert path is gone.
  • Don't accept a refactor whose intermediate commits are not shippable. That is a rewrite dressed as a series of commits.
  • Don't measure refactor success by lines removed. Lines removed is a vanity metric. Measure by complexity reduction (cyclomatic, cognitive), test coverage gained, and post-refactor defect rate.

Next