---
id: 22
title: "Quarantine bad architecture behind an interface, then replace it"
status: current
date: 2026-06-12
authors:
  - "Theo Zourzouvillys"
tags: [architecture, refactoring, design, process]
summary: "When a subsystem is complex and badly architected, quarantine it at its seam: write a clean adapter interface over the mess so the rest of the system depends on the contract, then build a better implementation behind it and expose the new interface directly."
supersedes: null
superseded_by: null
aliases: []
references:
  - id: strangler-fig
    title: "Martin Fowler — StranglerFigApplication"
    url: https://martinfowler.com/bliki/StranglerFigApplication.html
    abstract: "Martin 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."
---

## 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-fig](ref:strangler-fig) 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](/zfn/1-engineering-decision-records/). 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-14](/zfn/14-schema-first-apis-generate-clients/)), 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-19](/zfn/19-annotate-readonly-idempotent-endpoints/),
   [ZFN-14](/zfn/14-schema-first-apis-generate-clients/)) 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-23](/zfn/23-iterate-and-rewrite-implementations/): 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](/zfn/1-engineering-decision-records/) — seams and contracts between parts of a system.
- [ZFN-14](/zfn/14-schema-first-apis-generate-clients/) — define the seam as a real schema/interface; it
  also gives you conformance tests for the cutover.
- [ZFN-23](/zfn/23-iterate-and-rewrite-implementations/) — once it's behind a clean interface, replacing
  the implementation (or rewriting it) is low-risk.
- [ZFN-20](/zfn/20-deliberate-complexity-is-often-simpler/) — the part you're extracting is often the
  "easy" patch that grew into accidental complexity.
- Martin Fowler, [StranglerFigApplication](https://martinfowler.com/bliki/StranglerFigApplication.html);
  Michael Feathers, *Working Effectively with Legacy Code* (seams).

## Changelog

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