Theo Zourzouvillys

Field Note 23 current

Rewriting an implementation is fine — refactoring isn't always the answer

By
Theo Zourzouvillys
Published
Tags
architectureprocessrefactoringllm

TL;DR

Refactoring — improving structure incrementally while preserving behaviour — is a great tool, but it isn’t always the right one. It assumes the current structure is basically sound. When the structure or data model is wrong at the root, incremental refactoring can be slower than, and never arrive where, a clean reimplementation would. It’s fine — sometimes better — to iterate over a whole service implementation, or start it again from scratch. The thing to be precious about is the interface and the data model, not the code behind them. With clean, stable contracts (ZFN-14, ZFN-1) and conformance tests, the implementation is disposable: you can rebuild it behind the same interface and swap with confidence. And LLMs have dropped the cost of a well-specified rewrite far enough that “just rebuild it” is more attractive than it has ever been.

Context

“Never rewrite” is good advice aimed at a specific disaster: throwing away a mature codebase and losing all the accumulated knowledge — the thousand bug fixes and edge cases encoded in its corners — in a big-bang rewrite that takes years and ships worse. The failure there isn’t rewriting; it’s rewriting blind, with the hard-won knowledge living only in the code you deleted and no contract or tests to carry it forward.

Flip that around and the picture changes. If the edge-case knowledge is captured in a clean interface, a real data model, and conformance tests — not just in the implementation — then the implementation is no longer precious. It’s a replaceable expression of a contract you trust. Rebuilding it behind a stable, well-tested interface (ZFN-22) is low-risk and often faster than refactoring a fundamentally wrong structure toward a right one, because you’re not constrained by the existing shape on the way.

Two things make rewriting more attractive now than the old wisdom assumed:

  • Good interfaces and data models (which the rest of these notes push hard for) mean a component is scoped, contract-bound, and test-covered — exactly the conditions under which a rewrite is safe.
  • LLMs make producing a fresh, well-specified implementation dramatically cheaper. Given a clear interface, schema, and test suite, regenerating or re-attempting a component is now hours, not weeks — which lowers the cost of exploration and makes “try a cleaner take” a reasonable default rather than a last resort.

Recommendation

Treat implementations as replaceable behind stable contracts; reach for a rewrite when the structure, not the surface, is the problem.

  • Decide by where the problem lives. Surface-level mess in a sound structure → refactor. A wrong structure or data model that refactoring would only polish → reimplement against the known interface. Don’t refactor your way toward a destination the current shape can’t reach.
  • Protect the interface and the data, not the code. Keep the edge-case knowledge in the contract, the schema, and the tests (ZFN-14, ZFN-19) so the implementation can be thrown away without losing it. This is what makes a rewrite safe rather than reckless.
  • Rewrite behind a seam, and cut over incrementally. Put the component behind a clean interface (ZFN-22) and swap implementations there — shadow, compare against the old one, cut over per tenant/route, roll back if needed. A rewrite behind a tested interface is a controlled swap, not a leap.
  • Use cheap exploration. When a fresh take is hours of LLM-assisted work, try one — a second implementation against the same interface and tests is a low-cost experiment that often clarifies the right design even if you don’t ship it.
  • Don’t be precious about code you wrote. Sunk cost is not a reason to keep a bad structure. If a clean reimplementation is genuinely better and the contract makes it safe, do it.

Caveat. This is about implementations behind stable interfaces with tests. A blind big-bang rewrite of a mature system whose knowledge lives only in its code is still the disaster the old advice warns about — the mitigation is precisely the interface, data model, and tests, not bravado.

Consequences

Easier:

  • You can reach a good structure directly instead of refactoring a wrong one toward it forever.
  • Exploration is cheap: trying an alternative implementation against the same contract is a quick, safe experiment.
  • The interface/data-model discipline these notes advocate pays off here — it’s what makes implementations disposable and rewrites low-risk.

Harder:

  • It demands real interfaces, data models, and conformance tests up front; without them, a rewrite is the dangerous kind, so the safety depends on prior discipline.
  • Judging “refactor vs. reimplement” takes experience — and “rewrite” can be a seductive way to avoid understanding the existing system, which is its own failure mode.
  • A rewrite still has to actually reach parity on the edge cases; the tests and contract are what hold you to that.

References

  • ZFN-14 / ZFN-19 — schema’d interfaces and conformance tests are what capture edge-case knowledge so the implementation can be thrown away safely.
  • ZFN-22 — put the component behind a clean seam first; then rewriting behind it is a controlled swap.
  • ZFN-1 — clean contracts at seams are the precondition for disposable implementations.
  • ZFN-20 — getting the data model right is what makes later reimplementation cheap.

Changelog

  • 2026-06-12: First published as a Field Note.