Software Engineering

Refactoring Patterns

A practical catalogue of code-level and system-level refactoring techniques for improving internal structure without changing observable behaviour.

⏱ 11 min read

What it is

Refactoring, as defined by Martin Fowler, is "the process of changing a software system in such a way that it does not alter the external behaviour of the code, yet improves its internal structure." Refactoring patterns are named, repeatable transformations — from small method-level changes (Extract Method, Rename Variable) to large structural changes (Replace Conditional with Polymorphism, Introduce Parameter Object) — that improve code quality incrementally without rewriting from scratch.

Beyond code-level patterns, system-level refactoring patterns like the Strangler Fig (gradually replacing a legacy system by routing traffic to new implementations) and the Mikado Method (a systematic approach to large-scale refactoring with dependency graphs) address architectural evolution. The common thread is safety: refactoring always happens against a comprehensive test suite that verifies behaviour is preserved through each small, atomic step.

Why it exists

Code is written by developers who are learning the domain as they go. The first implementation reflects partial domain understanding; after working with the code, the team has much deeper knowledge of the right design. Refactoring is the mechanism for applying that knowledge without discarding what already works. Systems that are never refactored accumulate structural debt: classes grow too large, methods do too much, duplicate logic spreads across multiple places, and the relationship between code and domain becomes increasingly obscure. Refactoring reverses this entropy in small, safe steps.

When to use

  • As part of the TDD red-green-refactor cycle: after making a test pass, refactor before moving to the next feature.
  • When adding a feature is difficult because the current code structure makes it hard ("preparatory refactoring").
  • During code review, when reviewers identify code smells that were not addressed in the original PR.
  • When migrating legacy systems to modern architecture — use Strangler Fig rather than big-bang rewrites.
  • When a large-scale structural change is needed that would break the build for days if done in one step — use the Mikado Method to decompose it.

When not to use

  • Without tests: Refactoring without a test suite is dangerous — it is easy to accidentally change behaviour. Write characterisation tests first if none exist.
  • During active feature development on the same code: Refactoring and feature development in the same commit makes it impossible to separate what changed the behaviour from what changed the structure; keep them in separate commits or PRs.
  • On code about to be deleted: Refactoring code that will be removed in the next sprint wastes effort.
  • Gold-plating: Refactoring to the theoretically perfect design rather than to what is needed for the next feature is over-engineering; YAGNI applies to refactoring targets too.

Typical architecture


CODE-LEVEL REFACTORING PATTERNS (Fowler catalogue)
────────────────────────────────────────────────────
  Extract Method        — long method → named sub-method
  Extract Class         — class with multiple responsibilities → two classes
  Extract Interface     — concrete dependency → interface (enables testing/substitution)
  Move Method/Field     — method/field to the class that uses it most
  Encapsulate Collection— return unmodifiable collection; mutation via domain methods
  Introduce Parameter Object — multiple related params → value object
  Replace Temp with Query— temp variable → method call (enables reuse)
  Decompose Conditional — complex if/else → extracted methods with expressive names
  Replace Conditional with Polymorphism
                        — switch(type) → class hierarchy + virtual dispatch
  Inline Method         — method body is obvious; remove indirection
  Replace Magic Number  — literal 86400 → named constant SECONDS_PER_DAY

SYSTEM-LEVEL PATTERNS
───────────────────────
  Strangler Fig (Fowler/Feathers)
  ──────────────────────────────
  old system ←── proxy/router ──→ new system
  - Route specific endpoints to new implementation
  - Gradually shift traffic as coverage grows
  - Old system shrinks; new system grows
  - No big-bang cutover; rollback is always available

  Mikado Method (Ellnestam & Brolund)
  ────────────────────────────────────
  1. Try the desired change naively
  2. Note everything that breaks (the dependencies)
  3. Revert (git reset)
  4. Build a Mikado graph: goal → sub-goals → leaf tasks
  5. Start from the leaves (no dependencies); work inward
  6. Commit each leaf; never have a broken build

CHARACTERISATION TESTS (Feathers)
───────────────────────────────────
  Golden master tests for legacy code without tests:
  1. Run existing code with representative inputs
  2. Record all outputs as "golden master"
  3. New test: assert output equals golden master
  → Now you have a safety net for refactoring

Pros and cons

Pros

  • Improves code quality incrementally without the risk of a big-bang rewrite.
  • Strangler Fig enables safe replacement of legacy systems with zero-downtime migrations.
  • Mikado Method prevents "refactoring paralysis" where a large change stalls because it breaks too much at once.
  • Each refactoring step is small enough to verify immediately — IDE refactoring tools (IntelliJ, VS Code) make many transformations automatic and safe.
  • Improves the team's understanding of the domain as the code structure is aligned with domain concepts.

Cons

  • Requires comprehensive test coverage to be safe — refactoring into untested code risks silent behaviour changes.
  • Non-functional work is hard to justify to stakeholders who measure velocity by feature delivery.
  • Strangler Fig adds temporary complexity (the proxy layer) that must be removed after the migration completes.
  • Large refactoring efforts (Mikado) can span many sprints and require sustained team commitment.
  • Automated refactoring tools can miss cross-cutting concerns (logging, security annotations) that require manual review.

Implementation notes

Preparatory refactoring: Kent Beck's principle "make the change easy, then make the easy change" is the most important refactoring guideline. Before adding a feature, first refactor the code so the feature can be added cleanly. This separates the structural change (no observable behaviour change) from the functional change (new behaviour). Each type can be reviewed and verified independently, reducing cognitive load in code review.

Strangler Fig implementation: Deploy a routing layer (an API gateway, a reverse proxy, or a feature-flag-based facade) in front of both the old and new implementations. Begin by routing a small, well-understood slice of functionality to the new system. Verify in production with the old system available as a fallback. Gradually migrate more slices until the old system handles no traffic, then decommission it. The key safety property: at any point in the migration, reverting to the old system is a single routing configuration change — the old system is always running until explicitly decommissioned.

Common failure modes

  • Refactoring without tests: Making structural changes without a test suite means you cannot verify that behaviour was preserved; any such change carries high regression risk.
  • Never-ending Strangler Fig: The migration proxy stays in production indefinitely because there is always "one more endpoint" to migrate; set explicit milestones and decommissioning dates at project start.
  • Refactoring scope creep: Starting a small refactor and expanding it to touch unrelated code — "while I'm in here" — turns a safe, contained change into a large, risky one; stay focused on the target.
  • Skipping the revert in Mikado: When the naive attempt breaks things, the instinct is to fix each breakage rather than revert and build the dependency graph; this leads to the broken-build death spiral the method is designed to avoid.

Decision checklist

  • Does the code have sufficient test coverage to safely refactor (characterisation tests if none exist)?
  • Is the refactoring separated from feature changes into distinct commits or PRs?
  • For legacy system replacement, is a Strangler Fig proxy in place before new development begins?
  • For large structural changes, is a Mikado graph built before any code is changed?
  • Are automated IDE refactoring tools (Rename, Extract Method) used rather than manual text edits where possible?
  • Is there a plan to remove the Strangler Fig proxy once migration is complete?

Example use cases

  • Monolith to microservices: Strangler Fig routes /orders/* to the new orders service while the monolith continues handling all other requests; services are extracted one bounded context at a time over 18 months.
  • ORM migration: A codebase using raw JDBC needs to migrate to JPA. Mikado graph reveals 40 call sites, 3 transaction management patterns, and 2 custom type mappers. Leaf tasks (custom type mappers) are resolved first; transaction management is unified before the call sites are migrated.
  • Feature flag cleanup: A long-lived feature flag branch has accumulated complex conditional logic. Replace Conditional with Polymorphism refactoring replaces the if/flag checks with a strategy object that the flag selects at startup.

Further reading

  • Refactoring: Improving the Design of Existing Code — Martin Fowler (2nd ed., Addison-Wesley, 2018)
  • Working Effectively with Legacy Code — Michael Feathers (Prentice Hall, 2004)
  • The Mikado Method — Ola Ellnestam & Daniel Brolund (Manning, 2014)