← all posts

Refactoring legacy code without rewriting it

·2 min read ·Architecture · Best Practices · Engineering Culture

Every developer who encounters a legacy codebase eventually proposes a rewrite. The code is messy, inconsistent, and doesn't match how we'd build it today. Let's just start fresh.

This instinct is wrong almost every time. And I've had it, more than once.

Why Rewrites Fail

The existing system, messy as it is, encodes years of decisions. Some of those decisions are documented. Most aren't. They're in the behavior: the edge case that was handled after a production incident, the special logic for a specific customer, the workaround for a third-party API quirk.

A rewrite starts from scratch and re-discovers all of these the hard way, in production, after launch.

Netscape rewrote their browser in 2000. It took five years and nearly killed the company. Joel Spolsky wrote about this as 'the single worst strategic mistake any software company can make.' It still happens constantly.

The Strangler Fig Pattern

Instead of a big bang rewrite, build new behavior alongside the old and gradually replace the old with the new.

  1. New feature request? Build it the right way, in the right place
  2. Bug fix? Refactor the surrounding code while you're in there
  3. New module? Use the patterns you want for the entire module

Over time, the new way becomes the majority. The old code gradually gets replaced by necessity, not by a migration project.

Tests as a Safety Net

Before you refactor anything, write tests that capture the current behavior. Not tests for how you want it to work—tests for how it works now.

These tests are your safety net. As long as they pass, you haven't broken anything. With them, refactoring is safe. Without them, every change is a gamble.

The Right Question

Instead of 'should we rewrite this?', ask: 'what's the smallest change that makes this better and safer to change next time?' That question has a tractable answer. The rewrite question doesn't.