DEV Community

Archrad
Archrad

Posted on

Architecture as Code isn't enough — here's why it needs a compiler

Originally built this as a side project — sharing what I learned along the way.

Architecture decisions live in Confluence docs and Miro boards. They get reviewed once, approved, and then slowly become fiction as implementation diverges. A cache layer gets added. A direct DB call sneaks in. The agreed service boundary dissolves over three sprints.

There's no CI step for architecture. There's no compiler that says "this violates what you agreed to build." There's just a diagram that nobody updates.

I spent a long time thinking this was a discipline problem. Teams just need to be more rigorous, right? But I've come to think it's actually a tooling problem. Architecture has never been a machine-readable artifact. Diagrams are for humans. Docs are for humans. Nothing in your pipeline can read them — so nothing in your pipeline can gate on them.


The idea this builds on

Neal Ford and Mark Richards recently published a piece on O'Reilly Radar previewing their upcoming book Architecture as Code. They describe "fitness functions" — automated governance checks for architectural concerns, like unit tests but for architecture. The tools they reference (ArchUnit, NetArchTest, PyTestArch) are solid implementations of this idea.

But they all operate on code that already exists.

What I've been building is the pre-code version of that idea: a deterministic compiler that runs the fitness function on the architecture graph before any code is written. The intervention point is different — and so is the cost of fixing what you find.


The insight: architecture needs an IR

In compiler design, an IR (intermediate representation) sits between source code and target output. It's the stable, machine-readable artifact that validation, optimization, and code generation all operate on. Same input, same output, every time.

What if architecture had the same thing?

Not a diagram. Not a doc. A structured graph — nodes and edges — that a deterministic compiler can read, validate, and compile to runnable code. Something CI can gate on before a service repo even exists.

That's what I've been building: a deterministic compiler for system architecture.


What it looks like in practice

You describe your architecture as a graph. Here's a minimal example — a payment API talking directly to a database:

{
  "graph": {
    "nodes": [
      {
        "id": "payment-api",
        "type": "api",
        "name": "Payment API",
        "config": { "url": "/payments", "method": "POST" }
      },
      {
        "id": "user-db",
        "type": "database",
        "name": "User DB",
        "config": { "engine": "postgres" }
      }
    ],
    "edges": [
      { "from": "payment-api", "to": "user-db" }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

You run archrad validate on it:

⚠️  IR-LINT-DIRECT-DB-ACCESS-002: API node "payment-api" connects
    directly to datastore node "user-db"
    Fix: Introduce a service or domain layer between HTTP handlers
    and persistence.
    Impact: Harder to test, swap storage, or enforce invariants
    at a single boundary.

⚠️  IR-LINT-NO-HEALTHCHECK-003: No HTTP node exposes a typical
    health/readiness path (/health, /healthz, /live, /ready)
    Fix: Add a GET route such as /health for orchestrators and
    load balancers.
Enter fullscreen mode Exit fullscreen mode

These findings are deterministic. Same graph, same compiler version, same findings — every time. You can run this in GitHub Actions, fail the PR, attach the JSON output to a ticket, and show it to an auditor.

Now fix the graph — add a service layer node between the API and the database, add a health route — and re-validate. Clean gate. Export unlocks.

archrad export --ir graph.json --target python --out ./my-api
Enter fullscreen mode Exit fullscreen mode

Out comes a FastAPI project with OpenAPI, Dockerfile, docker-compose, and Makefile. Scaffold tied to a validated IR.


Where does the IR come from?

This is the first question everyone asks. Three paths:

Path A — Author YAML directly. Lighter than JSON, same result:

archrad yaml-to-ir --yaml graph.yaml --out graph.json
archrad validate --ir graph.json
Enter fullscreen mode Exit fullscreen mode

Path B — Ingest an existing OpenAPI spec. If your team already has one, each operation becomes an HTTP node automatically:

archrad ingest openapi --spec openapi.yaml --out graph.json
archrad validate --ir graph.json
Enter fullscreen mode Exit fullscreen mode

Path C — ArchRad Cloud. Type a natural language prompt in the browser, the planner converts it to a structured graph, download graph.json. The OSS compiler takes it from there — no cloud dependency on the blocking path.

The graph.json commits to your repo like any other source file. From that point everything is local, deterministic, and offline.


"But won't the IR file rot just like a Confluence doc?"

This is the right objection. Someone raised it when I posted about this on Reddit. I didn't have a complete answer then — so I shipped one.

archrad validate-drift re-exports from the IR in memory and diffs it against what's on disk. If generated code has been modified without updating the IR, CI fails:

archrad validate-drift -i graph.json -t python -o ./out

# ❌ DRIFT-MODIFIED: app/main.py
#    File differs from deterministic export for this IR
# archrad: drift detected — regenerate with `archrad export`
#    or align the IR.
Enter fullscreen mode Exit fullscreen mode

The IR becomes the authority. If code diverges from it, you know immediately — in local dev or in CI.

This doesn't fully solve the deeper problem the commenter raised — deriving IR from IaC or service mesh config is the right long-term answer and it's on the roadmap. But it closes the gap where generated code drifts silently from the IR that produced it.


How it fits into a real pipeline

graph.json committed to repo
        ↓
CI: archrad validate --fail-on-warning   ← blocks PR if architecture violated
        ↓
CI: archrad validate-drift               ← blocks PR if code drifted from IR
        ↓
CI: Spectral (lint generated openapi.yaml)
        ↓
CI: Snyk / Semgrep (scan generated code)
        ↓
Merge → deploy → operate
Enter fullscreen mode Exit fullscreen mode

ArchRad runs first because structural violations should block before other tools waste time scanning code that shouldn't exist yet. Findings output as --json for machine consumption — attach to PR comments, tickets, audit logs.


What this does NOT solve (honest scope)

I want to be direct about the limits:

Not semantic correctness. Drift detection means "code matches what the IR would generate." It doesn't prove the code is correct or matches prod behavior. That's your tests and your runtime.

Not full compliance attestation. The OSS layer catches structural and architecture heuristics — missing health routes, direct DB access, sync chain depth, high fan-out. Deeper policy (PCI, HIPAA, SOX rule packs) lives in ArchRad Cloud. OSS findings are supporting evidence for internal programs, not a substitute for external attestation.

Not a round-trip. Once you edit generated code, there's no built-in path back to the IR. Think of it as scaffold + contract validation, not full lifecycle sync.

The IR authoring cold start is real. Getting from "existing system" to a graph IR is work. The OpenAPI ingestion path helps for HTTP surfaces. IaC ingestion is on the roadmap. Hand-authoring YAML works for greenfield.


Can you add your own rules?

Yes — the lint rules are a typed visitor registry. Adding a custom rule is writing one function and appending it:

// my-org-rules/require-timeout.ts
export function ruleRequireTimeout(g: ParsedLintGraph): IrStructuralFinding[] {
  const findings: IrStructuralFinding[] = [];
  for (const [id, n] of g.nodeById) {
    const cfg = (n.config as Record<string, unknown>) || {};
    if (!cfg.timeout) {
      findings.push({
        code: 'ORG-001',
        severity: 'warning',
        layer: 'lint',
        message: `Node "${id}" has no timeout configured`,
        nodeId: id,
        fixHint: 'Add config.timeout to this node.',
      });
    }
  }
  return findings;
}

// In your pipeline:
import { LINT_RULE_REGISTRY } from '@archrad/deterministic';
LINT_RULE_REGISTRY.push(ruleRequireTimeout);
Enter fullscreen mode Exit fullscreen mode

Fork or wrap the package, write visitors that return IrStructuralFinding[], append to the registry. Same pattern the built-in rules use. Org-specific policy packs with more depth are a Cloud feature — but the extension point is open and documented.


The open-core structure

The deterministic engine — validate, architecture lint, export, drift detection — is fully open source under Apache-2.0:

👉 github.com/archradhq/arch-deterministic

npm install -g @archrad/deterministic
archrad validate --ir graph.json
Enter fullscreen mode Exit fullscreen mode

Want to try it without writing any IR? The repo ships fixtures that hit every lint rule:

npx @archrad/deterministic validate --ir fixtures/ecommerce-with-warnings.json
Enter fullscreen mode Exit fullscreen mode

ArchRad Cloud adds natural language authoring, org policy packs, compliance frameworks, and AI remediation — built on the same deterministic contract. The OSS compiler is what makes the cloud product's claims auditable. You can verify what the gate does before you buy anything.


Why the timing matters

The O'Reilly Architecture as Code book and the broader "architecture governance" conversation are converging right now. Academic papers on deterministic compilation for structured pipelines are appearing. The industry is waking up to the idea that architecture needs to be machine-readable.

The gap I'm filling is specific: the moment between architecture decision and first commit. Every existing tool — ArchUnit, Spectral, Snyk, Checkov — operates on artifacts that already exist. This is the only tool I know of that operates before any of those artifacts exist, on the architecture graph itself, with a deterministic gate you can put in CI.


What's your current approach?

I'm genuinely curious how other teams handle this. Do you enforce architecture through code review alone? Have you tried fitness functions or architectural tests? What worked, what failed?

Drop a comment — especially if you have a technical objection. Those tend to become features.

Top comments (0)