Nothing objects to a system acquiring fifteen ways to do one thing
Generated code is excellent locally and indifferent globally. Each change fits its immediate surroundings. No individual diff is the place where “we now have three approaches to background work” gets decided, so it never gets decided: it accumulates.
This was previously held together by two accidents. Writing was slow, so people reused rather than rewrote. And a small number of people wrote most of the code, so their preferences propagated by default. Both accidents are weaker now, and coherence has become something a team must defend deliberately or lose.
Write down the small number of ways
One page, in the repository, listing how this codebase does the recurring things. Not a style guide about brace placement: the decisions that matter and recur.
# How we do things here
Background work: Celery, via `tasks/`. Not threads, not cron, not
a queue in the database.
HTTP calls out: `lib/http.py` only. It sets timeouts and retries.
Configuration: environment variables, parsed once in `config.py`.
No `os.environ` anywhere else.
Database access: repository classes in `repo/`. Handlers never write SQL.
Errors to the user: raise a subclass of `UserFacingError`; the middleware
formats it. Never build an error response by hand.
Money: integer minor units, `Money` type, never float.
Time: timezone-aware UTC everywhere; convert at the edge only.Eight lines. It is the most useful page in most repositories, it takes an hour to write, and it does double duty now: it tells your colleagues and it tells the tools, since it belongs in whatever conventions file your editor reads.
Then make a machine enforce it
A written convention decays. An automated one does not. These checks are called fitness functions and they are cheaper to set up than most people expect.
# Python: import-linter, in setup.cfg
[importlinter:contract:layers]
name = Layered architecture
type = layers
layers =
api
services
repoThat contract fails the build if repo ever imports from api. One config block, permanent enforcement, and it catches the change that would slowly invert your dependency direction.
// JavaScript: dependency-cruiser
forbidden: [{
name: "no-ui-in-domain",
from: { path: "^src/domain" },
to: { path: "^src/(ui|components)" },
severity: "error"
}]Others in the same family, all free: ArchUnit for Java and .NET, eslint-plugin-boundaries, Rust’s module visibility, Go’s internal packages. And the crudest version, which works everywhere and takes two minutes:
# fail if anything outside lib/ imports requests directly
! rg -l '^import requests' --glob '!lib/**' src/A grep in CI is not elegant and it holds the line for years.
Give each area an owner
CODEOWNERS is one file and it puts a named human on every change to a sensitive area.
/billing/ @ananya @ravi
/auth/ @security-team
package-lock.json @platform-team
/docs/adr/ @tech-leadsThis is not gatekeeping. It is the mechanism by which somebody who holds the mental model of an area sees every change to it, which is exactly what stops the fifteen-ways problem, because that person will notice the sixteenth way when nobody else would.
Where the human judgement has to stay
Automation catches violations of decisions already made. It cannot make the decision.
Which means the architectural questions (should this be a separate service, what is the boundary between these two modules, do we need a queue here, is this the right data model) stay with people, and should stay with people, because they are bets on what your organisation does next.
A useful test for whether a decision is delegable: would being wrong be visible within a week? If yes, decide fast and cheaply and fix it if wrong. If the cost of being wrong shows up in eighteen months as a system nobody can change, that is a decision for someone who will still be there and who understands what the business is trying to become.
The property worth tracking
One question, asked of each service every quarter: can anybody on this team explain how this works?
Name them. If the answer is nobody, that is an operational risk of the same kind as an unmonitored backup, and it should be on the same list. It is also the earliest measurable form of the coherence problem, because a system with fifteen ways of doing things is precisely a system that nobody can hold in their head.
Every lesson here is open. Your progress is recorded once you pass the AI at Work exam.
Check your understanding
A team documents its layering rules in a well-written architecture guide. A year later, handlers routinely contain SQL. What was missing?
Pick the one you would defend. Your answer stays private.
Stuck on something?Ask Larissa, your AI tutor
