A new engineer asked me why the domain layer was importing the database package — “doesn’t an ADR say it shouldn’t?” It did. It had said so for two years. And for most of those two years, somewhere, the rule had been quietly broken: a helper here, a “just this once” import there, each one reasonable in its own pull request. The decision was still true on the wiki. It had stopped being true in the code months ago, and nobody noticed, because nothing was watching.

A decision you write down but can’t enforce isn’t a rule. It’s a wish with good formatting.

The diagram and the code drift apart in small, reasonable steps

Nobody decides to violate the architecture. It erodes one defensible commit at a time: a synchronous call added because the async path was slower to write, an import that crosses a boundary because the function you needed happened to live on the other side. Each diff looks fine on its own. The damage is cumulative and structural — invisible at the scale of a single change, obvious only when you step back and find the layering gone.

That’s exactly the scale a human review operates at, and exactly the scale it misses. Review works best on the architecture of a change, but a reviewer sees one PR, not the thousandth import that finally dissolved the boundary. You can’t ask a person to hold the whole dependency graph in their head on every commit. The wiki page can’t either; it’s prose, and prose doesn’t fail a build.

Make the constraint executable

The fix isn’t a better-written ADR. It’s moving the enforceable part of the decision out of prose and into something that runs.

“The domain must not depend on infrastructure” is not, at heart, an opinion — it’s a constraint on the import graph: files under domain/ may not import db/. That’s a rule a tool can evaluate on every commit, deterministically, and fail the build when it’s broken — before the violating import reaches main, not two years later. The cost of catching it moves from “an archaeology session when something finally breaks” to “a red check on the PR that introduced it.”

This is the same move as versioning a schema instead of mutating it: a rule everyone agrees on is worth nothing until something mechanical holds the line, because human discipline doesn’t scale to every commit across every contributor.

What stays in the document, and what becomes code

This doesn’t kill the ADR — it splits it. An architecture decision has two parts: the rationale (why we chose hexagonal layering, what we traded away, when we’d revisit it) and the constraint (domain imports nothing; the API talks to the DB only through a repository). The rationale belongs in prose — it’s context a tool can’t hold and a human needs. The constraint belongs in code, where it can be checked.

Keep writing the why down. Stop trusting prose to enforce the what.

When does this stop mattering?

If your whole system is two layers and a team that fits around one table, the boundaries live in everyone’s head, and a CI gate for them is ceremony. The moment a third contributor joins, or the codebase outgrows what one person can hold, the unenforced boundary is already eroding — you just won’t see it until the new engineer asks why domain imports the database.


An architecture is not what you decided; it’s what your code currently does. The only decisions that survive contact with a growing codebase are the ones something checks on every commit. Write the rationale for humans — and hand the boundary to a machine that never gets tired of enforcing it.


See also: archlint is the tool I built for exactly this — a deterministic import-boundary linter (Go, TypeScript and Python) that reads your layer rules and fails the build on a violation, with a packaged CI Action.