Field Note 22 current
Quarantine bad architecture behind an interface, then replace it
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-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.
- 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-14), designed around the caller’s needs, not the current implementation’s shape. This is the leverage point; spend the thought here.
- 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.
- Build the new implementation behind the same interface. Write the well-architected version against the clean contract, with conformance tests (ZFN-19, ZFN-14) so you can trust the swap. Cut over behind the interface — incrementally (per tenant, per route, shadow/compare) if the risk warrants.
- 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-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-1 — seams and contracts between parts of a system.
- ZFN-14 — define the seam as a real schema/interface; it also gives you conformance tests for the cutover.
- ZFN-23 — once it’s behind a clean interface, replacing the implementation (or rewriting it) is low-risk.
- 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.