Skip to content

Refactoring legacy applications

Practice · Chapter 14

Refactoring changes a system’s internal structure to improve it without altering external behavior (Martin Fowler’s definition, from Refactoring: Improving the Design of Existing Code). The goal on a legacy system is to make it safe and easy to change so you can add features, fix defects, improve design, and optimize — without touching business logic.

  • Respect the original team; there may be reasons behind decisions you don’t yet see.
  • Focus on substantive improvements, not stylistic rewrites of code you simply dislike.
  • Don’t change things whose impact you don’t yet understand.
  1. Make legacy code testable
  2. Remove redundant code
  3. Use tools to refactor
  4. Make small, incremental changes
  5. Transform monoliths to microservices

Many legacy systems lack automated unit tests, and some weren’t designed to be testable, making tests hard to add later. Prioritize adding them.

Benefits of unit tests

  • Lets people (even those unfamiliar with the system) change it safely — tests catch regressions.
  • Speeds debugging: a failing test narrows the problem to a recent change.
  • Acts as living documentation; even the act of writing tests builds team familiarity.

Refactoring for testability

  • Ideally tests exist before refactoring, but you may face a chicken-and-egg problem: some initial refactoring is needed just to make the first tests writable.
  • Alternative: write integration tests first (they exercise real dependencies, no mocks/stubs, so they need more setup). Run them before and during refactoring to confirm behavior is preserved. You end up with integration tests alongside unit tests.
  • Maintain tests going forward — update them as features and fixes land.

Where to start

  • By business criticality — cover the most important logic first.
  • By complexity — experienced teams may tackle the hardest parts first; less experienced teams may build up from simpler components. It can be split across the team.

See Testability.

Reducing total lines of code lowers complexity and improves understanding. Code analysis tools help identify removable code.

TypeDefinitionNote
Unreachable codeNo control-flow path ever reaches itConfirm with static analysis; if a bug caused it, fix the defect, else delete it. Version control preserves anything you might need later.
Dead codeExecutes but has no effect on output (e.g. computes an unused result)Subtly different from unreachable code.
Commented-out codeDisabled by comments instead of deletedEasiest to remove; version control already keeps history.
Duplicate codeIdentical/similar code in multiple placesViolates DRY; abstract into one location and route callers through it.

Eliminating duplication follows the DRY principle (see Minimizing complexity), so a change is made and tested in one place. All of this raises Maintainability.

Use IDEs and refactoring tools that detect improvement opportunities and preview changes before applying them. If your org lacks a tool, recommend purchasing licenses or adopt a free/open-source option.

  • Improvements need not be large or done all at once; incremental change is often best.
  • Pair each change with unit tests to avoid unintended consequences — leave the code better than you found it, functionality unchanged.
  • If dedicated improvement time isn’t granted, improve the area of code you’re already touching for a bug fix (the “boy scout” approach). Over time coverage of improvements grows.

A legacy monolith is a single self-contained unit with tightly coupled, interdependent components — hard to change when large.

Introducing microservices incrementally modernizes it:

  • Extract pieces of logic into small, focused, independently deployable services; callers interact via the service interface.
  • Implementations can change freely as long as the interface holds, and services can be modified independently, reducing ripple effects.
  • Better fault tolerance: one failing microservice doesn’t crash the whole app; combined with monitoring it can be restarted quickly, patched, or rolled back to a prior version without redeploying everything.
  • Software Architect’s Handbook (Packt, 2018), Ch.14 “Refactoring legacy applications”, pp. 1041-1054.