Aller au contenu principal
Engineering craft beyond tooling — design patterns, refactoring, code review, advanced testing strategy, and reading code you did not write.

Software Engineering Craft

Engineering craft beyond tooling — design patterns, refactoring, code review, advanced testing strategy, and reading code you did not write.

Refactoring Legacy Code

Legacy code is the code you fear — code that has no tests, unclear intent, and a long shadow of “no one is quite sure what depends on what”. Rewriting from scratch is the rookie instinct and the senior’s last resort. Refactoring in place is the actual craft: change the system a piece at a time, every step reversible, never stranded in a half-built state.

What Makes Legacy Hard

Three properties compound:

  1. No tests — there is no safety net, so any change is a guess.
  2. No single owner — the people who wrote it left; the current owners know less than you do.
  3. Hidden coupling — renaming a function breaks a script in another repo nobody remembered.

A greenfield rewrite☐all three problems, and reintroduces every bug the legacy code accumulated. In-place refactoring keeps the production bug history while improving the structure.

The Strangler Fig

Plant a new system alongside the old. Route a slice of traffic to it. When that slice works, route more. When the new system handles everything, retire the old.

callers → [ router ]
              ↓        ↓
            old system   new system
              ↑            ↑
              entire behavioural surface today    new slice today, everything tomorrow

Key properties:

  • Reversible at every step: roll traffic back if the new slice breaks.
  • Parallel: the new system can be built while the old still earns revenue.
  • No big-bang deadline: the only date that matters is “old retired”, and you only commit when the new provably handles the entire surface.

The pattern works at multiple scales: a microservice replacing a module, a database migrating per-tenant to a new schema, a UI framework transition behind a feature flag.

Seams

A seam is a place in existing code where you can alter behaviour without editing the surrounding code — usually by substituting a dependency. Examples:

Seam kindExampleWhen to expose
Object seamInject a fake at constructionThe class is instantiatable in tests
Link seamSwap a module import for a stub at test timeYou control the build / module map
Pre-processor seam#ifdef TEST eliminates unavailable APIsThe language supports it; rare in modern stacks

Most legacy code has no seams. The first refactor is introducing one: extract the dependency behind an interface, then the actual change happens against the interface. Extract Interface is the enabling move that unlocks everything else.

Branch-by-Abstraction

You need to refactor LegacyPaymentService to NewPaymentService without freezing development. The pattern:

  1. Create an abstraction (PaymentGateway) that the old service implements.
  2. Update every caller to depend on the abstraction, not the concrete service.
  3. Implement NewPaymentGateway against the same abstraction — run side-by-side.
  4. Flip routing, feature-flagged per tenant or per region.
  5. Retire LegacyPaymentGateway.

The team keeps shipping product changes during the refactor — they keep LegacyPaymentGateway updated while the new one builds. Big-bang refactors fail precisely because they pause product development; branch-by-abstraction never pauses it.

The Mikado Method

For invasive refactors where every change seems to spawn two prerequisites, the Mikado method works like a recursive undo tree:

  1. State the desired outcome (“extract parseConfig into its own file”).
  2. Try the change. If the build / test fails with a dependency, you have a new sub-goal (“need to expose Config type”). Revert.
  3. Recurse on each sub-goal until one succeeds cleanly.
  4. Apply in reverse-tree order, recommitting each layer.

The method turns “I broke everything” into “I have a list of achievable changes”. The repository is always building before each commit; you never strand the team in a broken state.

Continuous vs Big-Bang

PropertyBig-bangContinuous
VisibilityInvisible until “done”Each step ships to prod
RiskConcentrated at one deadlineSpread across many small bets
ReversibilityOften impossible once startedEach step is revertable
Cultural costThe author can be hero or scapegoatRefactor is a normal Tuesday

Always choose continuous, unless the product requires a single-cut migration (rarely; “we relaunch at Super Bowl”). Even then, prepare strangler underneath — the big-bang moment becomes a flag flip, not a knife-edge deploy.

Knowing When to Stop

The hardest discipline. A useful checklist:

  • The purpose of the refactor (faster onboarding? enable feature X? remove a bug class?) is achievable now. Declaring done is the goal, not polishing.
  • The cost per change has gone back up: the next move is hard because the code already supports what it needs to.
  • The diff has become a one-line cosmetic nit rather than a structural move. Stop.

Engineers over-refactor when they treat the codebase as a craft object rather than a tool. Most refactors should stop just before “perfect”, because the marginal refactor rarely ships to the customer but always costs the team’s review-time budget.

Practice Trajectory

  1. Identify one “scary” function nobody wants to touch in a codebase you use. Write three characterization tests for it; describe what changes in your willingness to edit it.
  2. Apply Extract Interface to one legacy dependency. Note what becomes testable that wasn’t before.
  3. Sketch a strangler-fig migration plan for any service you know: name the router layer, the slices, the rollback rule.
  4. Pick one improvement stalled on “needs refactoring first” and apply the Mikado method: draw the dependency tree, find a leaf you can ship today.
  5. Run a “what would I delete?” pass over the system after your refactor. Most legacy refactors net subtract code. If yours net adds, ask why.

When It’s the Right Tool

SituationTakeaway
High-risk module with no testsCharacterization tests first; refactor second
Migration that needs to ship productBranch-by-abstraction; never freeze development
Replacement that has clear slice pointsStrangler fig; route small slices first
Refactor blocked by tangled dependenciesMikado method; never strand the team in a broken state
“Refactor done, but I want to keep polishing”Stop. Ship. Refactor again when the next real need appears