archlint: Enforcing Architecture Boundaries in CI, Deterministically
Architecture decisions stay true on the wiki and rot in the code. archlint enforces the boundaries in architecture.json on every commit, no model.
I wrote separately about why architecture decisions rot: an ADR says “the domain must not import infrastructure,” and two years later you find it importing the database in a hundred places — because nothing was watching the rule. This post is the tooling side of that problem: a small, standalone Go CLI — archlint.
architecture.json: put the rule in code
You declare the layers and their allowed dependencies in one file:
{
"module": "github.com/acme/app",
"layers": {
"domain": ["internal/domain"],
"db": ["internal/db"],
"http": ["internal/http"]
},
"rules": {
"domain": [],
"db": ["domain"],
"http": ["domain", "db"]
}
}
domain may import nothing internal, db may import only domain, http may import both. Any other internal edge is a violation. A same-layer import is always allowed; [] means “may import no other layer.”
How it works
archlint check extracts every .go file’s imports with the standard library’s go/parser — no regex guessing, no compilation needed, exact. It maps each file and each import (after stripping the module path) to a layer, and reports the edge that breaks the rule, with file and line:
$ archlint check examples/sample
Scanned examples/sample against examples/sample/architecture.json — 2 layer(s).
1 boundary violation(s):
internal/domain/bad.go:6 domain → db is not allowed (import "github.com/acme/app/internal/db")
A violation exits 1 → the build goes red. The offending import is caught before it reaches main, not in an archaeology session two years later.
Design decisions
- Deterministic, no model. The whole point is a guardrail you can gate CI on: same diff, same verdict, no model in the loop. That’s the difference from telling an LLM to “review the architecture.”
- Zero dependencies (Go stdlib). The config is JSON for now — YAML adds a dependency, so it’s a follow-up.
- Go first.
go/parsergives Go imports exactly; a sharp MVP is done well in one language. TypeScript and Python, via their own parsers, are the next step.
In CI
From here on it’s the usual two lines of the workflows I build with GitHub Actions; all archlint asks is to be able to fail the job by returning a non-zero exit code on a violation.
- run: go install github.com/muhammetsafak/archlint/cmd/archlint@latest
- run: archlint check
Or with the packaged GitHub Action:
- uses: muhammetsafak/archlint@v0.3.0
Limits — the honest list
- Go, for now. Imports are read with
go/parser(exact); TS/Python via their own parsers is next. - Import boundaries, for now. It governs the dependency graph between layers. A synchronous call where an async one was required, or a direct DB query across a domain boundary, needs correlating runtime traces — a later phase.
- JSON config (YAML is a follow-up). The deterministic design is deliberate: it has to be gate-able.
Try it
Write your architecture.json, run archlint check — or run archlint check examples/sample to watch it catch a planted violation. If you tell me which boundary you want next (TS? Python?), I’ll queue it.
Experiments on this topic
Two languages sharing one queue without the serialization lock-in
A frozen JSON envelope in place of language-specific serialization; it became BabelQueue, a polyglot queue standard.
What it does today
The frozen JSON envelope has four languages — PHP, Python, Go, Node.js — reading the same bytes, with no sidecar and no broker plugin. Teams running a polyglot queue can pick up the spec and the SDKs today.
Local code review without letting the diff leave the machine
A provider-agnostic Go CLI that has a model review my diff without the diff leaving the machine; it became CommitBrief.
What it does today
Reviews code locally, with the diff never leaving the machine; the `Provider` interface lets Anthropic, OpenAI, Gemini and Ollama plug into one contract. Signed binaries are published — anyone who does not want to hand their diff to a cloud provider can install it.
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.