Guardrails — architecture that can fail a build
Every quality goal bound to a check that fails a build, a deploy or an eval. This is the concern arc42 has no section for, and the one that decides whether the rest of the spec describes your system or merely describes your intentions.
Spine concern · No arc42 equivalent · spine.yaml: guardrails
A guardrail is the automated check that fails when a quality goal is violated — a build rule, an architecture test, a runtime SLO alert, or an eval threshold. It names the goal it protects, the threshold that counts as a breach, and what happens when it trips. A quality goal with no guardrail is a preference, and the spec should say so out loud rather than let it sit in a table looking like a commitment.
The Basics
Durable. What this concern is responsible for, regardless of decade or stack.
An architecture you cannot detect a violation of is a description, not a constraint. Everything else in the spine states an intention; this concern is the only one that can tell you, without asking anybody, whether the intention is still true today.
What makes a check a guardrail
Three properties, and a check that is missing any of them is just a test. It names the goal it protects, so a failure explains itself instead of looking arbitrary. It can fail something that matters — a build, a deploy, a release gate — rather than emit a warning into a log nobody reads. And it has an owner, because a check that starts failing for an uninteresting reason and has no owner gets disabled within a week.
The naming is the part teams skip and the part that decides whether the guardrail survives its first inconvenient failure. A rule that says 'module boundary violation' gets deleted by whoever is blocked at 7pm. A rule that says 'this breaks QG-2, the change-failure-rate goal, per ADR-0009' gets a conversation instead.
The four kinds, in descending order of value
Build-time. Dependency direction, module boundaries, banned imports, the shape of the public surface. The cheapest and strictest kind: it runs in seconds, the feedback lands while the developer still has the context, and there is no ambiguity about the result.
Test-time. Architecture tests, contract tests between services, performance budgets, security scans. Slower, but able to assert things about behaviour rather than shape — that the retry path is idempotent, that a consumer's expectations still hold, that a request budget was not blown.
Runtime. SLOs with burn-rate alerts, error budgets, saturation thresholds. The only kind that measures the property you actually care about under real load, and the only kind that cannot block a bad change before it ships. You need it, and it is not a substitute for the two above.
Review-time. A human is asked to check something. Include it when nothing else can, and be honest that it is the weakest kind: it depends on attention, attention is finite, and it is the first thing to degrade under deadline — which is exactly when architecture violations get proposed.
The threshold is where the decision actually gets made
Setting a threshold at current behaviour ratchets nothing. It locks in whatever the system does today, including the parts that are already worse than you want, and it guarantees that the first legitimate improvement or regression both look identical to the check.
Derive the threshold from the quality goal, then compare it to the graph. If the goal says 2 seconds at p99 and the system currently does 3.4, you do not set the guardrail at 3.4 and call it protected. You either set it at 2 and accept a failing build until it is fixed, or you record that the goal is aspirational, with a date. Both are honest. Quietly encoding the status quo is not.
Decide what a breach does before it happens
Fail the build. Fail the deploy. Page someone. Open an issue. Warn only. Each is legitimate for different checks, and choosing in advance is the entire point — the alternative is choosing during an incident, under pressure, with a strong incentive to pick the one that unblocks the release.
Write it in the spec as a field. The moment 'what happens on breach' is written down next to the guardrail, the team has to admit which of their quality goals they are actually willing to stop a release for. That conversation is uncomfortable and it is the most valuable hour in the whole adoption.
A guardrail is complete when it has
- An id
- Referenced from the quality goal it protects and from any decision it enforces.
- A kind
- Build, test, runtime or review. If it is review, say so — do not dress a human up as automation.
- A threshold
- A number, derived from the goal rather than from last week's graph.
- A location
- The file or job that implements it, so the check and the claim cannot drift apart.
- A breach behaviour
- Fail, page, or warn — decided before the first failure, not during it.
In the spec
What the concern looks like once it is written down — the machine layer entries a build check or a coding agent resolves by id, and the prose beside them.
quality_goals:
- id: QG-1
attribute: availability
scenario: >-
During a single-AZ failure, checkout completes for 99.9% of
requests within 2s p99, without operator intervention.
priority: 1
guardrails: [G-1, G-2]
guardrails:
- id: G-1
checks: [QG-1]
kind: runtime
rule: Checkout availability SLO, multi-window burn-rate alert.
threshold: "99.9% over 30d; page at 14.4x burn over 1h"
enforced_by: prometheus/alerts/checkout.yaml
on_breach: page
- id: G-2
checks: [QG-1]
kind: test
rule: Load profile at 2x peak with one AZ removed.
threshold: "p99 < 2000ms, error rate < 0.1%"
enforced_by: .github/workflows/nightly-load.yml
on_breach: fail
- id: G-4
checks: [QG-2]
enforces: [ADR-0009]
kind: build
rule: No module may import another service's data layer.
threshold: "zero violations"
enforced_by: eslint.config.js (boundaries/element-types)
on_breach: failguardrails:
- id: G-7
checks: [QG-5]
kind: eval
rule: >-
Support-reply quality on the golden set. Runs on any change
to prompts/, the model version, or the retrieval config.
dataset: evals/support-golden-v4.jsonl
threshold: "pass rate >= 0.92; unsafe refusals = 0"
enforced_by: .github/workflows/evals.yml
on_breach: failThe Current
Reviewed 2026-09-09How it actually plays out in production now. Dated, because this is the part that decays.
Tooling stopped being the constraint some years ago. Every layer of a modern stack has something capable of failing a build, and most of it is free. What is still missing in almost every codebase is the binding between the check and the goal — hundreds of lint rules, zero rules that anyone can trace to an architectural intention.
Where the checks live now
Structure is covered by dependency and layering rules — ArchUnit in the JVM, ts-arch and dependency-cruiser or ESLint boundary rules in TypeScript, import-linter in Python. These are the highest-yield guardrails per hour invested and the most commonly absent.
Infrastructure is covered by policy-as-code evaluated against the plan rather than the running cluster: Conftest and OPA, Checkov, or the equivalent built into your platform. Catching a public bucket in a plan diff costs nothing; catching it in production costs an incident review.
Integrations are covered by contract tests and schema registries with compatibility modes set to something stricter than 'none'. Performance is covered by a small load profile in the pull request and the full one nightly. Supply chain is covered by SBOM generation, provenance attestation and licence policy, which moved from nice-to-have to procurement requirement over the last two years.
Runtime is covered by SLOs with multi-window burn-rate alerting. The single-threshold alert is still the most common implementation and still the wrong one: it pages for a blip and stays silent through a slow burn that will exhaust the month's budget by the 20th.
Cost became a first-class guardrail
Estimated infrastructure cost diffed on every pull request, with a threshold that fails or flags, is now ordinary practice rather than a finance-team fantasy. It works because it puts the number in front of the person who can still change the design cheaply.
The version that survives contact with a real team is a percentage and an escape hatch: flag when a change raises the monthly estimate beyond a set bound, and allow an explicit, labelled override that records who accepted the increase. A guardrail with no legitimate way past it gets routed around, and then you have neither the guardrail nor the record.
Platform teams own the library, not the rules
The pattern that scales is a shared pack of guardrails with sensible defaults, adopted by each service, with local overrides that require a reason and an expiry date. The platform team maintains the implementations; the service team owns which apply and why.
An override with no expiry is a decision. Record it as one. The failure this prevents is the exemption list that grows quietly for three years until the guardrail applies to nothing.
The honest state of practice
The metric worth tracking is not how many checks run. It is how many can fail a build today, and whether anyone can name the quality goal behind each one. In most repositories the answer is a large number and a small number respectively.
Warning-only mode is where guardrails go to die. It is a reasonable two-week ramp while the existing violations get burned down, and a permanent home for anything that was never going to be enforced. Put an expiry date on warning mode when you enable it.
Keep the pull-request set inside a wall-clock budget — ten minutes is the number past which developers start batching, branching around, and pushing at the end of the day to avoid the wait. Everything slower goes nightly, with a named owner and an alert, not into the pull-request path where it will eventually be disabled by someone with a release to ship.
Future-ready
What changes when agents write and operate the code. Opinionated on purpose.
When a large share of the diffs are written by agents, human review stops being the control that holds. It was already the weakest of the four kinds, it scales linearly with attention, and attention is the resource that agent-assisted development consumes first. Enforcement moves from the last line of defence to the primary one.
Review stops scaling before guardrails do
An agent can produce more changed lines in an afternoon than a team used to produce in a fortnight. The reviewing capacity did not change. Something has to absorb the difference, and the only candidate that scales without adding people is the set of checks that run automatically on every change.
This inverts how a team should spend its architecture effort. Time that went into review checklists and design forums now goes further encoded as checks — not because review is worthless, but because review is now the scarce input and should be spent on the changes the checks flagged as interesting.
Guardrails must be addressable before an agent can respect them
A check that only exists inside a CI configuration file teaches the agent nothing until after the build has failed. That is a slow, expensive loop: generate, push, fail, re-read, regenerate — and each cycle burns context and minutes.
Listed in the machine layer with an id, a rule and a threshold, the same constraint becomes something the agent reads before it writes. The check still runs and still fails; it just stops being the first time anyone learns the rule exists. This is the single highest-leverage reason for the machine layer to exist at all.
Evals are the guardrail for the parts that are not deterministic
Any LLM-shaped feature has quality goals that no unit test can assert: answer quality, refusal behaviour, tone, cost per call, latency under real prompts. The guardrail is an eval suite with a versioned dataset and a threshold, run in CI, treated exactly like the load test it resembles.
Three fields make it a real guardrail rather than a dashboard: the dataset version, the threshold, and what happens on breach. And the trigger set has to include prompt and model changes, because in an LLM feature those are deploys — a model version bump can move behaviour further than a fortnight of code changes.
The new failure mode: the agent weakens the guardrail
Given a failing check and an instruction to make the build pass, the shortest path is often to change the check. Loosen the threshold, add a path to the ignore list, mark the test skipped. The commit message will be reasonable and the diff will be small.
This needs a meta-guardrail, and it is cheap. Put arch/ and the guardrail configuration under code ownership so those files cannot be modified in a routine change. Assert that the count of enforced guardrails does not decrease. Require a superseding decision for any threshold change. None of this is hostile to agents — it is the same separation between what a contributor may change and what the team must agree, applied to a contributor that works much faster.
Flaky guardrails get deleted, and now they get deleted faster
A check that fails intermittently was always going to lose its credibility. With agents retrying and re-running builds automatically, an unreliable guardrail becomes noise at machine speed, and the pressure to remove it arrives in days rather than quarters. Determinism is now a functional requirement of the check, not a quality-of-life improvement.
How this concern fails
Named, because a failure mode you can name is one you can spot in your own repository before it costs you a quarter.
The warning-only guardrail
It runs, it prints, nothing stops. Six months later there are four hundred warnings and the signal is gone. Warning mode is a ramp with an expiry date, not a destination.
The threshold set to yesterday's graph
Encodes the status quo, protects nothing, and cannot ever fail until something changes for an unrelated reason. Derive thresholds from the quality goal; if the current system does not meet it, that is information, not a reason to move the line.
The orphan check
Nobody can say which quality goal it serves. It will be deleted the first time it blocks a release, and correctly so — an unexplained rule has no defence.
The nightly job nobody owns
Slow checks get moved out of the pull-request path, which is right, and then out of anybody's attention, which is fatal. Moving a guardrail to nightly means giving it an owner and an alert, otherwise you have deleted it with extra steps.
The permanent exemption list
Every override was justified on the day. None had an expiry. The guardrail now applies to the four files written since, and its pass rate is meaningless.
Go deeper
Frequently asked
What is the difference between a guardrail and a fitness function?
A fitness function is the check itself — the ArchUnit rule, the load profile, the SLO alert. A guardrail is that check plus the binding that makes it governance: the quality goal it protects, the threshold derived from that goal, what happens on breach, and where it is implemented. Same executable, different completeness. A fitness function with no named goal is the thing that gets deleted the first time it blocks a release.
How many guardrails should a system have?
Start with two that can fail a build and grow only when a real violation makes the case. Two enforced guardrails beat twelve described ones, because described guardrails create the impression of enforcement without any of the effect. A mature service typically settles at five to ten that block, plus runtime SLOs that page — small enough that everyone can name them, which is the actual test.
Should a guardrail ever be allowed to block a production release?
Yes, otherwise it is not a guardrail. The decision worth making in advance is which ones — a build-time boundary violation should block every time, whereas a nightly performance regression may flag rather than stop a hotfix. Write the breach behaviour into the spec next to each guardrail. Teams that skip that step end up deciding during an incident, when the incentive to bypass is at its strongest.
How do you stop a coding agent from weakening a guardrail to make the build pass?
Treat the guardrail configuration as protected the way you would treat production credentials. Put arch/ and the CI policy files under code ownership so a routine change cannot touch them, assert in CI that the number of enforced guardrails never decreases, and require a superseding decision for any threshold change. Then state the rule in the agent bundle so the agent proposes a decision instead of quietly editing the check.
Can evals be used as architecture guardrails for AI features?
That is the correct framing for them. An eval suite with a versioned dataset and a pass threshold is the only kind of check that can assert a quality goal for non-deterministic behaviour, and it belongs in the spec beside the deterministic checks rather than in a separate ML process. Make sure it triggers on prompt and model-version changes as well as code, because in an LLM feature those are deploys.
Does arc42 cover guardrails?
No, and that gap is the main reason The Spine exists as a separate artefact rather than a fork of arc42. arc42 section 10 asks you to write quality requirements as scenarios, which is genuinely good practice, but nothing in the template binds a scenario to an automated check. Enforcement is left entirely to the reader. The Spine makes it a first-class concern with its own ids, so an unenforced goal is visible as unenforced rather than indistinguishable from a protected one.
The other eleven concerns
Pages are being written one at a time. The ones without a page yet are still in the template, with prompts instead of prose.
- Decisions
- Quality goals
- Constraints
- Context & scope
- Solution strategy
- Structure
- Behaviour
- Deployment & operations
- Cross-cutting concepts
- Risks & debt
- Vocabulary
Guardrails
Take the template, not the idea
The zip, the single-file variant, the schema and the agent bundle. Nothing behind an email address.
Free to fork, modify and use commercially. No attribution required.