tklemenc.de

Why Interfaces Beat Inheritance in Long-Lived Codebases

After three years of growing a payment orchestration service, our inheritance tree looked like a subway map drawn by someone who hated tourists. Every new provider meant another subclass, another override, another silent assumption about process() that only failed in production on Fridays.

The problem we kept papering over

Inheritance promised reuse. What we got was shared mutable state and methods that meant different things on different branches of the tree. A change to the base class to support retries broke a legacy ACH adapter that depended on the old “fail loud” semantics. Unit tests still passed because they mocked the base class—the one place the real coupling lived.

Interfaces as contracts, not hierarchies

We rewrote the boundary as a handful of interfaces:

Each adapter implemented only what it needed. Composition replaced the god base class: a thin orchestrator wired gateways, stores, and reporters at startup. New providers became a package plus a registration line, not a new leaf on a brittle tree.

What improved

  1. Blast radius shrank. Changing Stripe behavior no longer risked ACH.
  2. Tests got honest. We tested adapters against the interface contract with table-driven cases, not inheritance spies.
  3. Onboarding got faster. Engineers read three small interfaces instead of a 900-line abstract class and its folklore comments.

Trade-offs we accepted

Interfaces do not remove complexity—they move it to wiring and naming. We still needed clear ownership of who constructs the graph, and we still forbade “just one more default method” on the interface. The discipline is the point: if the contract grows fat, you are designing inheritance again with extra steps.

Takeaway

Prefer small, stable interfaces and composition when systems will outlive the team that wrote them. Inheritance still has a place for narrow, truly is-a relationships. For everything else, make the contract explicit—and keep the hierarchy for org charts, not for process().

Tags: