ArchitectMind
  1. Home
  2. Tutorials
  3. Book 5 · Software Design & Design Patterns
  4. Volume VI
  5. What Is Refactoring?
Book 5 · Volume VI — Refactoring, Code Quality & Evolutionary Design · Topic 1.1

What Is Refactoring?

Behaviour preservation and structural improvement

11 min readStage 2 · Frameworks & design8 interview questions
Chapter
Chapter 1 — Refactoring Fundamentals
Prerequisites
Volumes I–III (principles and patterns)
You will be able to
Define refactoring precisely, distinguish it from feature work, bug fixing, optimisation and rewriting, explain what "observable behaviour" means in practice, recognise typical structural improvements, and apply the two-hats discipline in daily work
Covers
Behavior preservation; Structural improvement
On this page
  1. 1. Concept / Definition
  2. 2. Why It Exists
  3. 3. Core Concepts
  4. 4. How It Works Internally — The Two Hats and the Micro-Cycle
  5. 4A. Deep Dive — Refactoring vs Rewriting vs Re-architecting
  6. 5. APIs / Features / Syntax
  7. 6. Practical Examples
  8. 7. Common Problems & Mistakes
  9. 8. Performance & Production Considerations
  10. 9. Architecture & Design Considerations
  11. 10. Interview Questions & Answers

1Concept / Definition

Martin Fowler defines refactoring in two forms:

  • Refactoring (noun): a change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behaviour.
  • Refactor (verb): to restructure software by applying a series of refactorings without changing its observable behaviour.
Refactoring vs other code changes
Figure 1 — Refactoring vs other code changes
PropertyRefactoring
GoalBetter structure: readability, lower cost of change
ConstraintObservable behaviour unchanged
GranularitySmall named transformations (Extract Method, Move Field, …)
VerificationTests and compiler after each step
FrequencyContinuous — part of everyday development
Table 1 — Refactoring at a glance

2Why It Exists

The term emerged in the Smalltalk community (Opdyke's 1992 thesis, Beck and Cunningham's practice) and was popularised by Fowler's Refactoring (1999, 2nd edition 2018). Before it, "cleaning up code" meant risky, informal edits or big rewrites. Refactoring made improvement systematic: a catalogue of named transformations with precise mechanics, each safe on its own. This allowed Extreme Programming's idea of evolutionary design — design that grows incrementally with the code instead of being completed up front.

3Core Concepts

3.1 Behaviour preservation

"Observable behaviour" means what the users of the code can see: return values, outputs, side effects, exceptions and — for APIs — the public contract. Internal details (method names, class structure, private fields) may change freely.

ChangeBehaviour preserved?Refactoring?
Rename private method calc → calculateShippingCostYesYes
Extract a loop into a well-named methodYesYes
Change ArrayList to ArrayDeque internally, same resultsYesYes
Fix a rounding bug in the invoice totalNo (output changes)No — bug fix
Add a new discount typeNoNo — feature
Replace a slow algorithm with a faster one, same resultsFunctional yes; timing noUsually "performance optimisation" — do it separately
Rename a public REST fieldNo (contract changes)No — API change
Table 2 — What counts as behaviour-preserving

Behaviour preservation is judged at a boundary you choose. Renaming a public method is behaviour-preserving for the whole application (all callers updated) but not for external consumers of a published library. The wider the audience of an interface, the stricter the rules — which is why published APIs need deprecation cycles rather than refactorings.

JAVA
// Before — correct, but hard to read
public double total(List<Line> lines, String code) {
    double t = 0;
    for (Line l : lines) t += l.qty() * l.price();
    if ("VIP".equals(code)) t *= 0.9;
    return Math.round(t * 100) / 100.0;
}
 
// After — same results for every input, clearer structure
public double total(List<Line> lines, String customerCode) {
    double subtotal = subtotal(lines);
    double discounted = applyVipDiscount(subtotal, customerCode);
    return roundToCents(discounted);
}
private double subtotal(List<Line> lines) {
    return lines.stream().mapToDouble(l -> l.qty() * l.price()).sum();
}
private double applyVipDiscount(double amount, String customerCode) {
    return "VIP".equals(customerCode) ? amount * 0.9 : amount;
}
private double roundToCents(double amount) { return Math.round(amount * 100) / 100.0; }

Note a subtle trap: replacing the loop with mapToDouble(...).sum() changes the order of floating-point additions in some implementations, which can alter results in the last digit. Refactoring with doubles needs tests that pin the exact results — one reason money should be BigDecimal or a Money value object (a change that does alter behaviour and belongs in a separate commit).

3.2 Structural improvement

Refactoring aims at structure that makes the next change easier.

Structural improvement — before and after
Figure 2 — Structural improvement — before and after
Structural improvementTypical refactoringsEffect
Better namesRename Variable/Method/ClassCode explains itself
Smaller unitsExtract Method/ClassEach piece has one purpose
Better placementMove Method/FieldBehaviour next to its data
Less duplicationExtract + reuse, Pull Up MethodOne place to change
Clearer variationReplace Conditional with PolymorphismNew cases without edits
Explicit conceptsIntroduce Parameter Object, Value ObjectDomain language in types
Weaker couplingHide Delegate, Extract InterfaceChanges stay local
Table 3 — Kinds of structural improvement

Structural improvement is directional — towards a design that fits the current and next requirements, not towards an abstract ideal. Refactoring the same code "back" (Inline Method after Extract Method) is legitimate when the earlier structure no longer fits.

4How It Works Internally — The Two Hats and the Micro-Cycle

The two hats (Kent Beck)
Figure 3 — The two hats (Kent Beck)

Kent Beck's two hats metaphor: when adding function, you add tests and behaviour but don't restructure; when refactoring, you restructure but don't add behaviour. You switch hats often — sometimes every few minutes — but wear only one at a time.

The refactoring micro-cycle
Figure 4 — The refactoring micro-cycle

1. Spot a smell or an obstacle to the next change.

2. Green: make sure the relevant tests pass.

3. Step: apply one small refactoring (ideally an IDE-automated one).

4. Verify: compile and run fast tests.

5. Commit (or continue with the next step); repeat until the structure is right.

Commit typeContentReview focus
refactor:Structure only"Is it really behaviour-preserving? Is it clearer?"
feat: / fix:Behaviour change"Is the behaviour correct and tested?"
Table 4 — Separating commits by hat

Separating commits makes reviews faster (refactoring diffs can be large but mechanical) and makes rollbacks precise.

4ADeep Dive — Refactoring vs Rewriting vs Re-architecting

ActivityScopeBehaviourRisk profileTypical duration
RefactoringMethods, classes, modulesPreservedVery low per stepMinutes per step
Restructuring (large refactoring)Module/package boundariesPreservedLow–medium (many steps)Days–weeks, incremental
Re-architectingSystem structure (e.g. monolith → services)Mostly preserved, contracts may changeMedium–highMonths, incremental (strangler)
RewriteWhole applicationRe-implementedHigh ("second-system effect")Months–years, big bang
Table 5 — Degrees of structural change

The lesson from decades of failed rewrites (Netscape 6 is the classic example) is that incremental refactoring and strangling almost always beat a rewrite: the system keeps delivering value, knowledge embedded in the old code is preserved, and each step can be verified. Large-scale structural changes are therefore best treated as long sequences of refactorings with a clear direction (see Chapter 4 and Volume VII).

Refactoring at scale with tools. OpenRewrite applies refactorings as recipes across many repositories — for example migrating JUnit 4 to 5 or javax.* to jakarta.* — producing behaviour-preserving diffs reviewed like manual refactorings.

5APIs / Features / Syntax

Tool / featureRefactoring support
IntelliJ IDEA / Eclipse refactoring menusRename, Extract, Inline, Move, Change Signature, Introduce Parameter Object, Pull Up/Push Down
Java recordsRefactoring targets for value objects and parameter objects
Sealed interfaces + pattern matching switchSafe restructuring of type hierarchies (compiler checks exhaustiveness)
var, text blocksReadability refactorings
OpenRewrite (rewrite-maven-plugin)Automated, large-scale recipes
JUnit 5, AssertJ, ApprovalTestsBehaviour pinning before refactoring
Table 6 — Refactoring tools
XML
<!-- OpenRewrite: run a recipe that modernises code across a project -->
<plugin>
  <groupId>org.openrewrite.maven</groupId>
  <artifactId>rewrite-maven-plugin</artifactId>
  <configuration>
    <activeRecipes>
      <recipe>org.openrewrite.java.migrate.UpgradeToJava25</recipe>
    </activeRecipes>
  </configuration>
</plugin>

6Practical Examples

6.1 Classifying changes in a pull request

A pull request titled "Refactor pricing" contains: extracted methods, renamed classes, and a changed discount threshold. Reviewers ask the author to split it: the threshold change is a behaviour change needing its own tests and product approval.

6.2 Refactoring a record's accessor usage

JAVA
// Before: callers compute the same derived value everywhere
long nights = ChronoUnit.DAYS.between(stay.checkIn(), stay.checkOut());
 
// After: Move the computation into the record (behaviour-preserving)
public record Stay(LocalDate checkIn, LocalDate checkOut) {
    public long nights() { return ChronoUnit.DAYS.between(checkIn, checkOut); }
}
long nights = stay.nights();

6.3 Worked example — a refactoring session step by step

The task: add a "corporate" discount to InvoiceCalculator.total(). The method is 90 lines with nested conditionals. With the two hats:

StepHatChangeTests
1RefactoringAdd characterisation tests for existing totals (5 cases)New, green
2RefactoringExtract Method subtotal()Green
3RefactoringExtract Method discountFor(customer) with existing VIP ruleGreen
4RefactoringReplace conditional in discountFor with DiscountPolicy strategies (VIP, None)Green
5FeatureAdd CorporateDiscount strategy + testsRed → green
6RefactoringRename, tidy, remove dead parameterGreen
Table 7 — A preparatory refactoring session
JAVA
public sealed interface DiscountPolicy
        permits NoDiscount, VipDiscount, CorporateDiscount {
    Money apply(Money subtotal);
}
public record NoDiscount() implements DiscountPolicy {
    public Money apply(Money s) { return s; }
}
public record VipDiscount() implements DiscountPolicy {
    public Money apply(Money s) { return s.times(new BigDecimal("0.90")); }
}
// step 5: the feature
public record CorporateDiscount(BigDecimal rate) implements DiscountPolicy {
    public Money apply(Money s) { return s.times(BigDecimal.ONE.subtract(rate)); }
}

Kent Beck's summary of this approach: make the change easy (this may be hard), then make the easy change. Steps 1–4 took most of the time; step 5 took minutes and touched no existing logic.

6.4 Worked example — what went wrong in a "refactoring" that broke production

What happenedWhy it was not a refactoringLesson
Team "refactored" a JPA entity by renaming a fieldColumn mapping changed → queries returned nullsPersistence mappings are behaviour; test with real DB
Changed equals/hashCode while "cleaning up" a classObjects in HashSets no longer foundSemantics of equality are behaviour
Replaced SimpleDateFormat with DateTimeFormatter "for cleanliness"Different lenient parsing rulesLibrary swaps need behaviour tests
Moved a @Transactional method to another class and called it internallyProxy no longer applied → no transactionFramework semantics (proxies, AOP) are behaviour
Table 8 — Hidden behaviour that "refactorings" changed

In Java/Spring applications, observable behaviour includes framework semantics: persistence mappings, serialisation names, proxy-based annotations, reflection-based wiring. Refactorings touching these need integration tests, not only unit tests.

6.5 The core refactoring catalogue at a glance

Fowler's catalogue (2nd edition) names more than 60 refactorings. The table below lists the ones used most often in Java codebases, grouped by the structural improvement they achieve, with IntelliJ IDEA support.

GroupRefactoringWhat it doesIDE automated?
Composing methodsExtract Method / Extract FunctionTurn a fragment into a named methodYes
Inline MethodReplace a call with the method bodyYes
Extract VariableName an expressionYes
Replace Temp with QueryTurn a temporary into a methodPartly
Moving featuresMove Method / Move FieldRelocate behaviour or dataYes (static/instance)
Extract Class / Inline ClassSplit or merge classesPartly / Yes
Hide Delegate / Remove Middle ManAdjust navigation between objectsPartly
Organising dataReplace Primitive with ObjectIntroduce value objectsManual (records)
Encapsulate Field / CollectionHide mutable stateYes
Change Value to Reference and backAdjust identity semanticsManual
Simplifying conditionalsDecompose ConditionalName conditions and branchesVia Extract Method
Replace Conditional with PolymorphismType switches → subtypesManual
Introduce Guard ClausesFlatten nestingPartly ("invert if")
Simplifying callsRename, Change SignatureBetter names, parameter listsYes
Introduce Parameter ObjectGroup parametersYes
Replace Constructor with Factory MethodNamed creationYes
GeneralisationPull Up / Push Down MethodMove along the hierarchyYes
Extract Interface / SuperclassIntroduce abstractionsYes
Replace Inheritance with DelegationComposition over inheritanceYes ("Replace Inheritance with Delegation")
Table 9 — Refactorings used daily

Knowing the names matters: they make intentions explicit in commit messages and reviews ("Extract Class: PriceCalculator from OrderService"), and each name implies known mechanics and risks.

6.6 Measuring the impact of refactoring

Refactoring is justified by its effect on future change. Useful before/after evidence:

MeasureHow to obtainWhat improvement looks like
Cyclomatic / cognitive complexity of touched methodsSonarQube, PMDLargest method complexity down (e.g. 27 → 6)
Size of changes for similar featuresGit diff stats per ticketFewer files and lines per feature
Change couplinggit log analysis (CodeScene, custom scripts)Fewer files that always change together
Defect density in the areaIssue tracker linked to filesFewer bugs reported in refactored modules
Lead time for changes in the areaDelivery metricsFeatures in the area delivered faster
Table 10 — Evidence that refactoring paid off

Combining one structural metric with one delivery metric is usually enough to show stakeholders that refactoring time was an investment, not a cost.

7Common Problems & Mistakes

  • Calling any change "refactoring" — hiding behaviour changes in refactoring commits.
  • Mixing hats — adding features while restructuring.
  • Large, long-lived refactoring branches — merge conflicts and risk.
  • Refactoring without tests — no way to know behaviour was preserved.
  • Ignoring framework-level behaviour (JPA, Jackson, Spring proxies).
  • Refactoring code that will be deleted or never changed again.

8Performance & Production Considerations

ConcernReality
"Small methods are slow"The JIT inlines small methods; readability first, measure hot paths
Extra objects (value objects, strategies)Negligible in most code; escape analysis often removes allocations
Refactoring before optimisationWell-factored code is easier to profile and optimise
Deployment riskSmall refactoring commits deployed continuously are low risk
Table 11 — Performance and delivery realities

9Architecture & Design Considerations

  • Treat refactoring as part of every task, not as a separate project needing permission.
  • Use evolutionary design: defer structure until requirements justify it, then refactor towards it.
  • Define team conventions: separate commits, refactoring-friendly reviews, tests first.
  • At system scale, plan large restructurings as sequences of small refactorings with a target architecture.

10Interview Questions & Answers

Q1Basic

What is refactoring?

Refactoring is changing the internal structure of code to make it easier to understand and cheaper to modify, without changing its observable behaviour. Examples include renaming a variable to reveal its intent, extracting a method from a long one, moving a responsibility to a better class or replacing conditional logic with polymorphism. Each step is small and verified by tests, so the code stays working throughout. Refactoring is not a separate project or a rewrite; it is a continuous habit, done as part of everyday work, often just before adding a feature to make the change easier.

Q2Basic

Is fixing a bug a refactoring?

No. A bug fix changes observable behaviour: the code did something wrong, and after the fix it does something different. Refactoring, by definition, preserves behaviour. The distinction matters in practice. If a bug fix and a refactoring are mixed in the same change, reviewers cannot tell which lines change behaviour, tests that fail are harder to interpret, and a rollback removes both. The disciplined approach is to refactor first, without behaviour change, to make the bug easy to fix; then fix the bug in a separate, small change with a test that reproduces it.

Q3Intermediate

What are Kent Beck's "two hats"?

Kent Beck described programming as wearing one of two hats at any time. With the "adding function" hat, you add new behaviour and new tests, without restructuring existing code. With the "refactoring" hat, you restructure code without adding behaviour and without changing tests, except when an interface change requires it. You switch hats often, sometimes every few minutes, but you are always aware which one you are wearing. Mixing the two makes it impossible to know whether a failing test means a broken refactoring or an incomplete feature. Keeping them separate keeps each step small, verifiable and safe.

Q4Intermediate

What does "observable behaviour" mean?

Observable behaviour is what users of the code can see at the boundary you are preserving: return values, side effects such as database writes or messages sent, exceptions thrown, and the public contract, such as method signatures and API formats. Internal details, such as private methods, temporary variables and class structure, are not observable and can change freely. The boundary matters: refactoring inside a class preserves its public methods, while refactoring a service must preserve its REST API and events. Choosing the boundary explicitly tells you which tests must stay green and which interfaces you must not change.

Q5Senior Engineer

Why separate refactoring commits from feature commits?

Separate commits make each change easy to review: a reviewer can check a pure refactoring for unchanged behaviour, often mechanically, and focus attention on the feature commit, where behaviour does change. Tests are easier to interpret, because a failure in a refactoring commit can only mean the refactoring was wrong. If a problem appears in production, the feature can be reverted without undoing the refactoring, or vice versa. Separate commits also make the history useful for understanding why code changed. Many teams use commit prefixes, such as refactor:, to make this visible.

Q6Senior Engineer

Give examples of hidden behaviour in Spring applications that refactorings can break.

Spring applications contain behaviour that depends on names and structure through reflection, so refactorings that compile can still break them. Renaming an entity field changes JPA column mappings or JPQL queries; renaming a DTO field changes JSON property names and breaks API clients; changing fields used by equals and hashCode breaks collections; moving a @Transactional method into the same class as its caller bypasses the proxy, so transactions silently stop; and renaming a bean or property can break @Qualifier, @Value or configuration binding. Integration tests, contract tests and explicit annotations such as @Column and @JsonProperty protect against these.

Q7Technical Architect

Why is incremental refactoring usually preferred over rewrites?

A rewrite stops delivering value for months or years while the new system catches up, and it usually loses knowledge that is embedded in the old code: edge cases, bug fixes and business rules nobody documented. The new system must also reach feature parity with a moving target. Incremental refactoring keeps the system running and delivering throughout, makes each step small and verifiable, and lets the team stop or change direction at any point. Patterns such as Strangler Fig allow even large replacements to happen incrementally. Rewrites are justified only when the old platform cannot be evolved, and should still be delivered in slices.

Q8Solution Architect

How do you enable refactoring across many teams?

Make refactoring a normal part of work, not a separate activity that needs permission. Include it in the definition of done, so leaving code better is expected. Invest in fast, reliable test suites and continuous integration, because refactoring without tests is risky. For large-scale changes across many repositories, such as framework upgrades or API migrations, use automated tools like OpenRewrite. Provide architectural direction, such as target module structures and coding guidelines, so that independent refactorings by many teams converge rather than diverge. Track technical debt visibly and reserve capacity for the larger refactorings that need planning.

Newsletter

Get new tutorials every week

New ArchitectMind tutorials and interview questions, once a week. No spam, unsubscribe anytime.

Almost done! Check your inbox and click the confirmation link to complete your subscription.
Keep going in Book 5

Software Design & Design Patterns

Volume I · Topic 1.1

Software Design

Software Design Foundations & Principles

Volume I · Topic 1.2

Why Software Design Matters

Software Design Foundations & Principles

Volume I · Topic 1.3

Good vs Poor Design

Software Design Foundations & Principles

Volume II · Topic 1.1

Objects

Object-Oriented Design & SOLID

Volume II · Topic 1.2

Classes

Object-Oriented Design & SOLID

Volume II · Topic 1.3

Relationships

Object-Oriented Design & SOLID

Volume III · Topic 1.1

What Is a Design Pattern?

Creational & Structural Design Patterns

Volume III · Topic 1.2

Pattern Categories

Creational & Structural Design Patterns

Volume III · Topic 1.3

Pattern vs Framework

Creational & Structural Design Patterns

Volume IV · Topic 1.1

Strategy Pattern

Behavioral Design Patterns

Volume IV · Topic 1.2

Strategy in Java

Behavioral Design Patterns

Volume IV · Topic 1.3

Strategy vs Conditional Logic

Behavioral Design Patterns

Volume V · Topic 1.1

Layered Architecture

Enterprise & Application Design Patterns

Volume V · Topic 1.2

Service Layer

Enterprise & Application Design Patterns

Volume V · Topic 1.3

Repository

Enterprise & Application Design Patterns

Volume VI · Topic 1.2

Why Refactor?

Refactoring, Code Quality & Evolutionary Design

Volume VI · Topic 1.3

Refactoring Safety

Refactoring, Code Quality & Evolutionary Design

Volume VII · Topic 1.1

Method-Level Design

Advanced Design & Architecture-Level Design Decisions

Volume VII · Topic 1.2

Class-Level Design

Advanced Design & Architecture-Level Design Decisions

Volume VII · Topic 1.3

Module-Level Design

Advanced Design & Architecture-Level Design Decisions