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

Refactoring Safety

Tests, small changes and continuous verification

10 min readStage 2 · Frameworks & design8 interview questions
Chapter
Chapter 1 — Refactoring Fundamentals
Prerequisites
Topics 1.1–1.2
You will be able to
Build a test safety net that verifies behaviour rather than implementation, refactor in small steps that keep the build green, rely on automated IDE refactorings where possible, verify continuously from IDE to CI, and recover quickly when a step goes wrong
Covers
Tests; Small changes; Continuous verification
On this page
  1. 1. Concept / Definition
  2. 2. Why It Exists
  3. 3. Core Concepts
  4. 4. How It Works Internally — A Safe Refactoring Workflow
  5. 4A. Deep Dive — Where Automated Refactorings Are Not Enough
  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

Refactoring safety is the set of practices that ensure refactorings really are behaviour-preserving: a test safety net that detects behaviour changes, small changes that are easy to verify and undo, and continuous verification that runs checks after every step, at every stage from the IDE to CI.

PracticeProtects against
Tests (behaviour-focused)Accidental behaviour changes
Small changesHard-to-locate errors, long broken periods, merge conflicts
Automated refactoringsMechanical mistakes (missed references, wrong scopes)
Continuous verificationLate discovery of problems
Version control disciplineInability to undo quickly
Table 1 — Safety practices

2Why It Exists

Refactoring changes code that works. Without safety, every change risks introducing bugs that users find first — and teams quickly learn to "not touch working code", which is how systems rot. The safety practices make refactoring routine: developers can change structure confidently because any behaviour change is detected within seconds or minutes, close to its cause.

3Core Concepts

3.1 Tests

The safety net for refactoring
Figure 1 — The safety net for refactoring

Tests suitable for refactoring verify behaviour at stable boundaries, not implementation details.

JAVA
// Brittle: tied to implementation → breaks on every refactoring
@Test
void callsRepositoryAndCalculatorInOrder() {
    service.placeOrder(cmd);
    InOrder o = inOrder(repo, calculator);
    o.verify(calculator).calculate(any());
    o.verify(repo).save(any());
}
 
// Robust: verifies observable behaviour → survives restructuring
@Test
void placedOrderHasDiscountedTotalAndIsStored() {
    OrderId id = service.placeOrder(cmdFor(vipCustomer(), line("A", 2, "100.00")));
    assertThat(orders.findById(id)).get()
        .extracting(Order::total)
        .isEqualTo(Money.inr("180.00"));
}
Test characteristicWhy it matters for refactoring
Tests public behaviour, not private methodsPrivate structure can change freely
Uses real collaborators where cheap (value objects, policies)Fewer mocks to rewrite when responsibilities move
Fast (unit tests in ms)Can run after every small step
Deterministic (fixed clocks, no network)Failures mean real changes
Clear failure messagesLocate the cause quickly
Table 2 — Refactoring-friendly tests

Coverage is necessary but not sufficient. Line coverage shows which code ran, not whether assertions would catch a change. Mutation testing (PIT) introduces small changes into the code and checks that tests fail; surviving mutants reveal weak spots in the safety net.

XML
<plugin>
  <groupId>org.pitest</groupId>
  <artifactId>pitest-maven</artifactId>
  <configuration>
    <targetClasses><param>com.shop.pricing.*</param></targetClasses>
    <mutationThreshold>80</mutationThreshold>
  </configuration>
</plugin>

When no tests exist, write characterisation tests first — tests that capture current behaviour, right or wrong (Chapter 4, Topic 4.2).

3.2 Small changes

Small steps vs big leap
Figure 2 — Small steps vs big leap

Each step should be small enough to verify immediately and cheap to undo.

Step size principlePractice
One refactoring at a timeExtract Method, run tests; Move Method, run tests
Keep it compilingNever leave the code in a non-compiling state for long
Parallel change (expand–contract)Add new structure, migrate callers, remove old structure
Commit oftenEvery green state is a potential commit
Undo, don't debugIf a step breaks tests unexpectedly, revert and take a smaller step
Table 3 — Principles of small changes

Parallel change (expand–migrate–contract) keeps even signature changes safe:

JAVA
// Expand: add new method alongside old one
public Money price(Product p, PricingContext ctx) { … new implementation … }
 
@Deprecated(forRemoval = true)
public Money price(Product p, Customer c, LocalDate d) {  // old signature
    return price(p, new PricingContext(c, d));  // delegates
}
// Migrate: move callers to the new method one by one (tests green after each)
// Contract: delete the deprecated method when no callers remain

3.3 Continuous verification

Continuous verification
Figure 3 — Continuous verification

Verification happens at several speeds:

StageCheckFeedback time
IDECompiler, inspections, automated refactoring previewsInstant
Local test runUnit tests of the module (continuous testing plugins)Seconds
Pre-commit / pre-pushModule tests, formatting, static analysis< 1–2 minutes
CI pipelineAll tests, integration tests, ArchUnit, Sonar quality gate, mutation tests (nightly)Minutes
DeploymentCanary/feature flags, monitoringMinutes–hours
Table 4 — Verification stages

The faster the feedback, the smaller the steps can be — and the safer refactoring becomes. Teams should treat test suite speed as a first-class quality attribute.

4How It Works Internally — A Safe Refactoring Workflow

1. Pull latest trunk; run tests (start from green).

2. If the area lacks tests, add behaviour tests or characterisation tests.

3. Apply one refactoring — prefer IDE automation (preview the change).

4. Run fast tests; if red and the cause isn't obvious within a minute, revert.

5. Commit with a refactor: message; push frequently (trunk-based or short-lived branches).

6. CI runs the full suite and static checks.

7. Repeat until the target structure is reached; then start the feature (other hat).

How safe is each kind of change?
Figure 4 — How safe is each kind of change?

4ADeep Dive — Where Automated Refactorings Are Not Enough

IDE refactorings understand Java semantics but not everything that references code:

Hidden referenceExampleSafeguard
ReflectionClass.forName("com.shop.LegacyPricing")Search for strings; integration tests
Spring configurationBean names in @Qualifier("pricingService"), @ConditionalOnPropertyContext-loading tests (@SpringBootTest smoke)
SerialisationJackson field names from getters/records; Java serializationContract/approval tests of JSON
PersistenceJPA entity/field names in JPQL strings, native SQLRepository tests with Testcontainers
Templates / scriptsThymeleaf expressions, SpEL, shell scriptsRendering tests; grep
External consumersPublic APIs, events, library usersContract tests; deprecation instead of rename
Table 5 — Hidden references
JAVA
// Guard against accidental JSON contract changes during refactoring
@Test
void orderResponseJsonIsStable() throws Exception {
    String json = mapper.writeValueAsString(sampleOrderResponse());
    Approvals.verifyJson(json);          // fails if field names/structure change
}

Architecture tests protect structural rules while you refactor:

JAVA
@ArchTest
static final ArchRule domainIndependentOfSpring =
    noClasses().that().resideInAPackage("..domain..")
               .should().dependOnClassesThat().resideInAPackage("org.springframework..");

5APIs / Features / Syntax

ToolRole in safety
JUnit 5, AssertJBehaviour tests with expressive assertions
ApprovalTests / snapshot testsPin complex outputs (JSON, reports)
TestcontainersReal databases/brokers for integration tests
PIT (pitest)Mutation testing — quality of the safety net
JaCoCoCoverage reports to find untested areas
ArchUnitStructural rules as tests
IntelliJ refactorings, "Safe Delete"Semantics-aware changes
Git (small commits, git revert, git bisect)Quick undo and fault isolation
Table 6 — Safety tooling

6Practical Examples

6.1 Making tests refactoring-proof

A test suite of 400 tests mocking every collaborator broke 120 tests when PricingService was split into PricingPolicy and TaxPolicy. Rewriting the tests to use real value objects and policies (mocking only I/O gateways) reduced breakage in the next refactoring to 3 tests.

6.2 Using git bisect after a bad refactoring series

When a subtle bug appeared after 25 small refactoring commits, git bisect run mvn -q test -Dtest=InvoiceTotalsTest found the offending commit in five iterations — possible only because each commit was small and green.

6.3 Worked example — safety net before refactoring a pricing module

StepActionResult
1JaCoCo coverage of pricing package48% line coverage; branches for VIP and bulk discounts untested
2Characterisation tests for 30 real orders (inputs from production logs, outputs recorded)Current behaviour pinned
3PIT mutation testingMutation score 52% → surviving mutants in rounding and thresholds
4Add targeted tests for thresholds and roundingMutation score 86%
5Refactor in 18 small commits (Extract Method, Extract Class, Replace Conditional)All tests green after each
6Deploy behind no flags (behaviour unchanged)No incidents
Table 7 — Building and using a safety net
JAVA
@ParameterizedTest
@CsvFileSource(resources = "/pricing/characterisation.csv", numLinesToSkip = 1)
void priceMatchesRecordedBehaviour(String customerType, String sku,
                                   int qty, String expectedTotal) {
    assertThat(pricing.total(order(customerType, sku, qty)).amount())
        .isEqualByComparingTo(expectedTotal);
}

6.4 Worked example — expand–contract across modules

Renaming CustomerService.find(Long) to CustomerDirectory.byId(CustomerId) used by 40 call sites in 6 modules:

PhaseChangeDeployable?
ExpandAdd CustomerDirectory.byId(CustomerId); old method delegates to itYes
MigrateUpdate call sites module by module (6 small PRs)Yes after each
ContractRemove old method (IntelliJ "Safe Delete" confirms no usages)Yes
Table 8 — Parallel change in practice

No big-bang PR, no long-lived branch, no broken build — the whole change took two days of interleaved work.

6.5 Worked example — continuous verification pipeline

YAML
# .github/workflows/ci.yml (excerpt)
jobs:
  build:
    steps:
      - run: ./mvnw -q verify                 # unit + integration tests, ArchUnit
      - run: ./mvnw -q sonar:sonar -Dsonar.qualitygate.wait=true
  mutation:
    if: github.event_name == 'schedule'      # nightly
    steps:
      - run: ./mvnw -q -pl pricing pitest:mutationCoverage
GateThreshold
TestsAll green
Sonar quality gate on new codeNo new critical issues; duplication < 3%
ArchUnitNo violations
Mutation score (nightly, critical modules)≥ 80%
Table 9 — Quality gates supporting refactoring

6.6 Recovering when a refactoring step goes wrong

Even with small steps, something occasionally breaks. The rule is revert, don't debug — then retry with a smaller step.

SituationResponse
Tests fail right after a step and the cause is not obvious in a minutegit checkout . / IDE local history → back to green; redo in smaller pieces
A failure appears several commits latergit bisect to find the step; revert that commit
Refactoring deployed and an incident occursRoll back the deployment (refactoring commits are isolated); analyse offline
Behaviour difference discovered that tests didn't catchAdd a test that reproduces it first, then fix — the safety net improves
Table 10 — Recovery strategies
BASH
# find the refactoring commit that broke invoice totals
git bisect start HEAD v2.14.0
git bisect run ./mvnw -q -pl billing test -Dtest=InvoiceTotalsTest
git revert <bad-commit>
EnablerWhy it makes recovery cheap
Small, single-purpose commitsRevert affects only one step
Separate refactoring and feature commitsReverting a refactoring doesn't remove features
Fast testsBisect iterations take seconds
IDE local historyUndo beyond the last commit
Table 11 — What makes recovery cheap

Teams that practise small steps rarely need these tools — but knowing that recovery is cheap is exactly what gives developers the confidence to refactor.

7Common Problems & Mistakes

  • Refactoring without tests — "it looked mechanical".
  • Tests coupled to implementation (over-mocking, private method tests).
  • Big steps — hours of broken code, then debugging.
  • Slow test suites — developers stop running them.
  • Trusting coverage as proof of a good safety net.
  • Ignoring hidden references (reflection, JSON, SQL, Spring config).

8Performance & Production Considerations

AspectGuidance
Test suite speedKeep unit tests < 1–2 minutes per module; parallelise
Integration testsReuse Testcontainers; run per module
Mutation testsTargeted modules, nightly
Production verificationMonitor error rates after deploying refactorings; canary where risk is higher
Table 12 — Performance of the safety net

The safety net is only useful if it is fast enough to run constantly, so manage its performance like production code. Run isolated JUnit 5 tests in parallel, split unit and integration suites between Maven Surefire and Failsafe, and reuse Testcontainers instances through singleton containers instead of starting a database for every test class. Use Gradle or Maven build caching and test only affected modules on pull requests, with the full suite on trunk. Track flaky tests and fix or quarantine them quickly, because a flaky suite teaches developers to ignore red builds. Limit PIT to changed code with its incremental analysis. After deploying refactorings, compare error rates and p99 latency with the previous release to catch behaviour changes the tests missed.

9Architecture & Design Considerations

  • Design for testability (dependency injection, pure domain logic) to enable safe refactoring.
  • Treat fast feedback (build and test time) as an architectural concern.
  • Use architecture tests to lock in structural decisions during refactoring.
  • Prefer trunk-based development with small commits over long refactoring branches.

10Interview Questions & Answers

Q1BasicConcept

What is the essential precondition for refactoring?

A solid, fast suite of tests that detects behaviour changes, because refactoring is by definition a change to structure that must not change behaviour, and tests are the only practical way to confirm that after every step. Fowler calls this the essential precondition. Solid means the tests check observable behaviour at stable boundaries with meaningful assertions, not just that code ran; fast means you can run them after each small change. When the area has no such tests, the first job is to write characterisation tests that capture what the code currently does, right or wrong. Without that safety net every refactoring is a gamble, and teams soon stop refactoring.

Q2BasicConcept

Why take small steps?

Because small steps keep the cost of a mistake tiny. If you make one refactoring, such as Extract Method or Move Method, and then run the tests, any failure is caused by that one change, so you can locate it immediately or simply revert and try a smaller step instead of debugging. Small steps also keep the code compiling and the build green, so you can commit, integrate and even deploy at almost any moment, avoiding long-lived branches and painful merges. A series of small green commits also makes later fault-finding with git bisect fast. Large leaps leave hours of broken code and failures with many possible causes.

Q3IntermediateCode

What makes a test refactoring-friendly?

It verifies observable behaviour at a stable boundary, not how the code is arranged internally. In practice that means calling a public API or use case and asserting on results or persisted state, for example that a placed VIP order has a discounted total and is stored, rather than verifying with Mockito that a calculator was called before a repository. It uses real collaborators where they are cheap, such as value objects and policies, and mocks or fakes only I/O gateways, so moving responsibilities between classes does not break it. It is fast, deterministic thanks to fixed clocks and no network, and its failure message points to the cause. Such tests survive restructuring.

Q4IntermediateConcept

What is parallel change (expand–contract)?

Parallel change, also called expand-migrate-contract, lets you change an interface without a breaking big-bang step. In the expand phase you add the new structure alongside the old one, for example a new price(Product, PricingContext) method, make the old method delegate to it and mark it @Deprecated(forRemoval = true). In the migrate phase you move callers over one by one, keeping tests green and the system deployable after each step, possibly across several pull requests and modules. In the contract phase, once no callers remain, which IntelliJ's Safe Delete can confirm, you remove the old method. The same technique works for database columns, REST fields and event schemas.

Q5Senior EngineerTricky

Why is coverage not enough to trust a safety net?

Coverage only shows which lines or branches were executed by tests, not whether any assertion would fail if that code behaved differently. A test that calls a pricing method and asserts nothing gives full coverage and no protection. Mutation testing measures what you actually care about: tools such as PIT make small changes to the code, like turning > into >= or removing a method call, and run the tests; a mutant that survives reveals a behaviour change the safety net would miss. Use coverage to find untested areas, and the mutation score on critical modules, for example nightly with an 80 percent threshold, to judge the strength of the tests.

Q6Senior EngineerTricky

What references can automated IDE refactorings miss?

IDE refactorings understand Java's type system, so they miss anything that refers to code by string or from outside the compiled project. Typical cases are reflection such as Class.forName, Spring bean names in @Qualifier, property names in @ConditionalOnProperty and SpEL expressions, Jackson field names derived from getters or record components, Java serialisation, entity and field names inside JPQL strings or native SQL, Thymeleaf templates and scripts, and external consumers of public APIs, events or libraries. Renaming a record component can therefore silently change a JSON contract. Safeguards are context-loading smoke tests, approval tests of JSON, repository tests with Testcontainers, contract tests, and deprecation instead of renaming for public APIs.

Q7Technical ArchitectDesign

How does CI support refactoring?

CI is the shared, objective verification step after each small change. Every push runs the full unit and integration test suite, including tests against real databases with Testcontainers, plus architecture tests with ArchUnit or Spring Modulith and a static-analysis quality gate such as SonarQube on new code. Slower checks like mutation testing run nightly on critical modules. Because refactorings are integrated as small commits on trunk or short-lived branches, a failure points to a recent, small change that can be reverted quickly. CI also catches what local runs miss, such as other modules affected by a rename. To support refactoring, the pipeline itself must stay fast, ideally under ten minutes.

Q8Solution ArchitectDesign

What organisational practices make refactoring safe at scale?

Refactoring at scale is a property of the engineering system, so the practices are organisational. Architect for testability, with dependency injection and framework-free domain logic, so most behaviour can be tested quickly. Treat build and test time as an architectural concern and keep pipelines fast. Use trunk-based development with small, separate refactoring and feature commits, and avoid long-lived refactoring branches. Lock in structural decisions with architecture tests and quality gates, and run mutation testing on critical modules. Use feature flags and canary releases where risk is high, and monitor error rates after deployments. Finally, make refactoring part of every feature rather than a project that needs special approval.

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.1

What Is Refactoring?

Refactoring, Code Quality & Evolutionary Design

Volume VI · Topic 1.2

Why Refactor?

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