Skip to content
Amine Mekki
Back to articles

Boundaries before abstractions

Most painful codebases don't suffer from too little abstraction. They suffer from abstractions in the wrong places. Boundaries come first; abstractions are what you earn afterwards.

  • Architecture
  • Clean code
  • Refactoring

Ask what makes a codebase hard to work in and people usually answer "spaghetti", meaning not enough structure. My experience is closer to the opposite: the worst codebases are full of structure. Base classes with one subclass. Generic services parameterized for cases that never arrived. Helpers wrapping two lines of a library call. Structure everywhere, boundaries nowhere.

The distinction

A boundary is a line you agree not to cross casually: domain logic does not import the HTTP framework; the UI does not know the shape of database rows; one module talks to another through an explicit surface. An abstraction is a mechanism: an interface, a base class, a wrapper. Abstractions are one way to implement a boundary, but you can have a hundred abstractions and no boundaries at all.

Boundaries are what actually buy you change tolerance. When the quoting rules are separated from the controllers that expose them, changing a rule is a one-file diff. When the frontend consumes a typed data-access layer instead of raw CMS responses, swapping how content is fetched touches one module. Neither of those requires inventing a pattern; both require deciding where the lines are and holding them.

Why premature abstraction fails

An abstraction created before the second use case is a guess about the future. Most guesses are wrong, and a wrong abstraction is worse than duplication: it couples every caller to a shape the domain never wanted, and unwinding it costs more than writing the duplicate ever would. Duplication is cheap to fix precisely when you finally understand the pattern. That is the moment the abstraction becomes earned rather than speculative.

A working order

Draw the boundaries first: what is domain, what is infrastructure, what talks to what. Keep the code inside each boundary as plain as possible: functions, modules, explicit types. Let duplication exist until it hurts, then extract the abstraction the real cases demand. In that order, abstractions stay small, honest and replaceable. In the reverse order, you get a framework nobody asked for on top of a domain nobody separated.