Tooling · Conventions

The Doc Said "Done."
When to Stop Adding Checks

Every time a manual audit finds drift, the obvious fix is to write the check that would have caught it. The harder question is when to stop writing them. For me the yield curve answered it: the early batches of new rules each found three or four violations, the next batch found two, and the last found none in code. It started with a doc that said a naming cleanup was done when only half of it was.

This happened in my example project, which implements the same backend design in five languages side by side. Its root guide is explicit about Repository method names: find<Noun>s for any lookup, single record or list, save<Noun> for writes, no update method. Checking whether the code followed it turned up violations in four of five languages. Go and FastAPI used a bare Save/save with no noun. The Card domain (the second Bounded Context, added later) had a "dedicated findOne plus a separate findAll" shape in Java, Kotlin, Go, and FastAPI that the convention doesn't allow. Only NestJS was clean everywhere.

The First Finding: "Done" on One Side Only

Kotlin's own repository-pattern.md claimed this cleanup was already finished. It was, for AccountRepository, the write-side interface. The parallel read-side interfaces (AccountQuery, CardQuery, PaymentQuery, RefundQuery) still had the old names. Nobody had lied; the fix had landed on one side of a symmetric pair and never made it to the other. The doc today says so plainly, because it now doubles as its own regression note:

Even if a doc says "done," if an interface's renaming was actually missed (as CredentialQuery.findByUserId once was), it surfaces as a harness FAIL.

Before Writing a Rule, Ask Why It Recurred

The interfaces were fixed by hand in four languages, verified with a full build and test run each, and pushed. Then came the more useful question: why had this specific, simple, well-documented convention drifted in four languages independently? Each language already had an architecture checker (a script that statically checks code against the documented rules), and those checked plenty: file placement, layer purity, import direction. None of them checked exact method-name conventions. A separate docs drift script (check_docs_drift.py) checked something adjacent but unrelated: whether a doc's claims match the file tree, not whether a method is spelled the way the doc says it should be. No tool existed that could have caught this, in any language, ever. That was the root cause, not carelessness in any one implementation.

The First Run Tests the Diagnosis

A repository-naming rule went into all five languages, including NestJS, which was already compliant, purely as a regression guard against the next drift. It works as a blocklist: flag findBy*, a bare findAll, count*, a bare save, a bare delete, on *Repository/*Query interfaces. It caught three more previously unnoticed violations immediately, all in the Auth/Credential domain, across Go, FastAPI, and Kotlin. If the tool-gap diagnosis had been wrong, the new rule would have found nothing new to find.

Widening the Search Without Forcing Rules

The next question was obvious once the first one paid off: what other conventions were documented but not enforced? More passes with the same question followed, adding fifteen structural rules in total, not all of which applied to every language. Domain-layer isolation, no cross-aggregate references within a Bounded Context, no direct env-var access outside config modules, aggregate-ID hex format, the exact four-field error-response shape, soft-delete filtering on every query, and more.

Not every rule applied to every language, and forcing one where it didn't fit would have traded signal for noise. Each language investigated applicability first and skipped or narrowed a rule with a documented reason rather than shipping a false-positive machine. FastAPI skipped an interface/infrastructure-isolation rule because its own docs mandate direct infrastructure instantiation inside Depends factories; there's no DI container to isolate against. Go skipped a no-public-setters rule because Go structs are conventionally all-exported in this codebase, so the rule's premise about encapsulation didn't hold for the language. Go's own pass did find one more violation on its own: interface/http importing infrastructure/auth directly, a boundary the new domain-layer-isolation rule was built to catch.

The next batch added five more rules and found two more bugs. FastAPI's PaymentModel and RefundModel were missing a deleted_at column entirely, unlike every other model in the same codebase. It also disproved something earlier notes had flagged as a known gap: Java's rate-limit filter, believed still unwired, turned out to have been fixed already, in an earlier, unrecorded commit. A "known gap" written down once is a snapshot, not a live fact, and needs re-checking against current code before it gets cited again.

Stop When the Yield Curve Goes Flat

The last batch added four more rules across all five languages and turned up zero code violations anywhere. What it did find were two leftover doc claims (one language's tactical-ddd.md still describing the codebase as single-domain, a line nobody had touched since before the second Bounded Context existed) and one honest non-applicability: Go's stack has no ORM at all, so an ORM-autosync rule doesn't apply, and got documented as explicitly not applicable rather than forced through.

The yield curve was the point

The early batches found three to four violations each. The one after that found two. The last found zero code bugs and two stale doc lines. That drop isn't evidence the later work was wasted. It's the closest thing to proof that the earlier batches had closed most of the low-hanging cross-language drift this category of check can find. The goal was never to keep finding bugs forever; it was to find out when to stop, and a flat yield curve is the only honest way to learn that.

"The doc said done" turned out to be less a lie than a claim nothing could verify: a self-report with no regression guard behind it, in a codebase with five parallel implementations any one of which could drift back unnoticed. What changed wasn't just the naming. It was that "done" stopped being something a doc could merely assert.

Further reading

docs/architecture/repository-pattern.md (the naming convention, in my example project that implements the same backend design in five languages) · the Kotlin repository-pattern.md (the note explaining why it is now checked automatically) · repository_naming.go (one language's version of the regression guard) · Compliance as Code: What an Architecture Checker Catches, and What It Keeps Missing (the three kinds of drift that placement checks keep missing)