---
id: 23
title: "Rewriting an implementation is fine — refactoring isn't always the answer"
status: current
date: 2026-06-12
authors:
  - "Theo Zourzouvillys"
tags: [architecture, process, refactoring, llm]
summary: "Refactoring isn't always right. When the structure is wrong at the root, it's fine — often better — to rewrite an implementation from scratch. Clean interfaces and data models make the implementation disposable: stable contract, swappable internals. LLMs make it cheaper still."
supersedes: null
superseded_by: null
aliases: []
---

## 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/14-schema-first-apis-generate-clients/),
[ZFN-1](/zfn/1-engineering-decision-records/)) 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](/zfn/22-extract-complexity-at-the-seam/)) 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/14-schema-first-apis-generate-clients/),
  [ZFN-19](/zfn/19-annotate-readonly-idempotent-endpoints/)) 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](/zfn/22-extract-complexity-at-the-seam/)) 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/14-schema-first-apis-generate-clients/) / [ZFN-19](/zfn/19-annotate-readonly-idempotent-endpoints/)
  — schema'd interfaces and conformance tests are what capture edge-case knowledge so the implementation can
  be thrown away safely.
- [ZFN-22](/zfn/22-extract-complexity-at-the-seam/) — put the component behind a clean seam first; then
  rewriting behind it is a controlled swap.
- [ZFN-1](/zfn/1-engineering-decision-records/) — clean contracts at seams are the precondition for
  disposable implementations.
- [ZFN-20](/zfn/20-deliberate-complexity-is-often-simpler/) — getting the data model right is what makes
  later reimplementation cheap.

## Changelog

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