Own Every Layer

Everything outside the core must be replaceable — and what that rule cost us

Belief 03 · 9 min read ·

We wrote one rule down before we wrote the product: everything outside the core domain must be replaceable. No cloud, database, storage, payment, email, or model vendor may appear in the core domain’s types or logic. Each of those categories sits behind an interface we own.

Rules adopted after the fact are not rules. They are regrets with better formatting. So this one went into the repository before there was anything to apply it to.

What the rule actually forbids

It is stricter than “use dependency injection.” The domain may not import a vendor SDK, may not name a vendor in a type, and may not branch on which vendor is configured. A PaymentProvider, a StorageProvider, an LLMProvider — the domain knows those interfaces and nothing about who implements them.

The tempting exception is always the same: one vendor has a feature no interface models cleanly, and threading it through properly costs a day. We have not taken that exception, because the first time you do, the rule becomes a preference, and preferences lose to deadlines.

The second constraint that makes it real

An interface alone proves nothing. The constraint that gives the rule teeth is that every provider category ships a working free default — local disk for storage, a Postgres table for the queue, in-memory cache, a logging email provider, deterministic mock inference. Not a stub that throws. A working implementation the system genuinely runs on.

That is what proves the seam is real. An abstraction with exactly one implementation is not an abstraction; it is indirection with extra steps. Two implementations, one of which the whole test suite runs against, is a seam you can trust.

What it cost

This is the part usually left out, so here it is plainly.

  • The first month was slower. Designing an interface before you have felt the problem it abstracts is genuinely harder than writing the direct call. We got some shapes wrong and revised them.
  • More indirection to read. A new engineer tracing a request passes through a boundary that would not exist in a vendor-direct codebase. That is a real, permanent readability tax.
  • We give up vendor-specific ergonomics. Managed services are pleasant precisely because they leak into your code. Refusing that means writing more of the glue ourselves.
  • Free defaults need maintaining. Two implementations per category is more surface than one, and the free path has to keep working or the seam quietly rots.

What it bought

  • Production runs at no mandatory monthly infrastructure cost at our current scale — an outcome of the rule, not a separate initiative.
  • Moving off a free tier is a configuration change. When something outgrows its default we change a value; we do not schedule a migration.
  • No vendor’s pricing page, deprecation notice, or rate-limit change gets a vote in our roadmap.
  • The test suite runs with no API keys, no network, and no credentials, because the free defaults are deterministic.

When we would break it

If a category ever has exactly one credible implementation for the foreseeable life of the system, and modelling it as an interface makes the domain harder to understand for no available second implementation, the abstraction is ceremony. We would rather delete it than defend it.

That has not happened yet. Every category we assumed was single-vendor turned out not to be, usually within a year.

Own every layer.