Theo Zourzouvillys

Field Note 22current

Quarantine bad architecture behind an interface, then replace it

By
Theo Zourzouvillys
Published
Tags
architecturerefactoringdesignprocess

TL;DR

When part of the system is complex and lacks good data architecture — the gnarly module everyone is afraid to touch — don’t try to fix it in place, and don’t let the rest of the system keep reaching into its guts. Pull it out at its seam: define a clean adapter interface that captures what the rest of the system actually needs, implement that interface on top of the existing mess so everything now depends on the contract instead of the mess, then build a properly-architected implementation behind the same interface and cut over. Finally, expose the new interface directly and delete the adapter and the old path.

This is the strangler-figMartin Fowler — StranglerFigApplicationMartin Fowler's pattern for replacing a legacy system gradually: build the new system around the edges of the old one, route more functionality through it over time, and let it slowly strangle the original — avoiding the risk of a big-bang rewrite by keeping the system working throughout.martinfowler.com ↗ move: contain the bad part behind a boundary, then replace it incrementally and safely, instead of either living with it forever or attempting a terrifying big-bang rewrite of something tangled into everything.

Context

Badly-architected complexity has two costs: the mess itself, and the way the rest of the system grows into it. Callers reach past any boundary into its internals, depend on its quirks, and couple to its bad data model — so the blast radius of the mess spreads, and it becomes un-replaceable not because it’s big but because everything touches it. At that point you feel stuck with two bad options: keep patching it, or stop the world for a rewrite that has to land all at once.

There’s a third option, and it’s almost always the right one: introduce a seam. A seam is a place where you can change behaviour on one side without editing the other — exactly the contract-at-a-boundary idea from ZFN-1Field Note · currentZFN-1 — Keep engineering decision recordsRecord significant engineering decisions as short, versioned markdown files — context, decision, consequences. Write one for cross-team contracts, directional principles, hard-to-reverse choices, and conventions others must follow. Cite them instead of re-arguing.Open ZFN-1 →. Once the messy subsystem sits behind a clean interface, the rest of the system no longer depends on how it works, only on what it promises — and the implementation becomes something you can replace at your own pace. The interface is also where you get the real prize: a chance to define the clean contract and data model the subsystem should have had, decoupled from the implementation that got it wrong.

Recommendation

Extract, adapt, replace, expose — in that order.

  1. Find the seam and define the interface. Identify where the messy subsystem meets the rest, and write the interface the rest of the system should depend on — a clean contract and data model (ZFN-14Field Note · currentZFN-14 — Define every API with a schema, and generate the clientsDefine every API with a machine-readable schema (OpenAPI, Protobuf, GraphQL) as the source of truth, and generate clients and server stubs from it — never hand-roll request-building and JSON parsing. Hand-written clients drift and break silently; check schema compatibility in CI.Open ZFN-14 →), designed around the caller’s needs, not the current implementation’s shape. This is the leverage point; spend the thought here.
  2. Adapt the existing mess to it. Implement that interface as an adapter over the current implementation, and route all access through it. Nothing reaches past the interface anymore. This step changes no behaviour — it just relocates the dependency from the mess to the contract, and it’s where you contain the blast radius.
  3. Build the new implementation behind the same interface. Write the well-architected version against the clean contract, with conformance tests (ZFN-19Field Note · currentZFN-19 — Annotate read-only and idempotent endpoints; make every mutation idempotentAnnotate every endpoint as read-only (safe) or idempotent, in the schema, so infrastructure can retry, route to replicas, and cache safely. Make every state-changing endpoint idempotent (idempotency keys for create/charge/send); a non-idempotent retry double-applies.Open ZFN-19 →, ZFN-14Field Note · currentZFN-14 — Define every API with a schema, and generate the clientsDefine every API with a machine-readable schema (OpenAPI, Protobuf, GraphQL) as the source of truth, and generate clients and server stubs from it — never hand-roll request-building and JSON parsing. Hand-written clients drift and break silently; check schema compatibility in CI.Open ZFN-14 →) so you can trust the swap. Cut over behind the interface — incrementally (per tenant, per route, shadow/compare) if the risk warrants.
  4. Expose the new interface directly and delete the scaffolding. Once the new implementation is the only one, drop the adapter and the old code path so you’re not maintaining the boundary indefinitely.

The discipline that makes it work: the rest of the system depends only on the interface at every step, so the messy internals — and later the replacement — are free to change without rippling outward.

Consequences

Easier:

  • The blast radius of the bad part is contained the moment the adapter lands; the rest of the system is insulated by the contract.
  • Replacement becomes incremental and reversible (you can cut over a slice, compare, roll back) instead of a big-bang rewrite of something entangled with everything.
  • You get to fix the data model and contract at the interface — often the actual source of the mess.
  • It composes with ZFN-23Field Note · currentZFN-23 — Rewriting an implementation is fine — refactoring isn't always the answerRefactoring 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.Open ZFN-23 →: behind a clean interface, rewriting the implementation is cheap and safe.

Harder:

  • The adapter is real, throwaway work, and for a while you run two implementations and a boundary between them.
  • Designing the right interface over a mess takes understanding the mess well enough to know what the contract should be — the hard part, and the part worth doing carefully.
  • A leaky interface that exposes the old model’s quirks just relocates the problem; the boundary has to be genuinely clean to pay off.

References

  • ZFN-1Field Note · currentZFN-1 — Keep engineering decision recordsRecord significant engineering decisions as short, versioned markdown files — context, decision, consequences. Write one for cross-team contracts, directional principles, hard-to-reverse choices, and conventions others must follow. Cite them instead of re-arguing.Open ZFN-1 → — seams and contracts between parts of a system.
  • ZFN-14Field Note · currentZFN-14 — Define every API with a schema, and generate the clientsDefine every API with a machine-readable schema (OpenAPI, Protobuf, GraphQL) as the source of truth, and generate clients and server stubs from it — never hand-roll request-building and JSON parsing. Hand-written clients drift and break silently; check schema compatibility in CI.Open ZFN-14 → — define the seam as a real schema/interface; it also gives you conformance tests for the cutover.
  • ZFN-23Field Note · currentZFN-23 — Rewriting an implementation is fine — refactoring isn't always the answerRefactoring 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.Open ZFN-23 → — once it’s behind a clean interface, replacing the implementation (or rewriting it) is low-risk.
  • ZFN-20Field Note · currentZFN-20 — The simplest-looking system is often the most complex to live withThe system that's simplest to stand up often isn't simplest to live with — it skips the correctness edge cases, so bugs and inconsistency surface fast. A more deliberate design has more parts but fewer surprises, and is often the simpler one over time.Open ZFN-20 → — the part you’re extracting is often the “easy” patch that grew into accidental complexity.
  • Martin Fowler, StranglerFigApplication; Michael Feathers, Working Effectively with Legacy Code (seams).

Changelog

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