Patterns
A pattern is a reusable architecture shape — the rules every "REST service in front of a database" or "fan-in queue worker" in your org has to follow. Patterns are JSON Schema documents you author once and reference from many architecture files. Customers in regulated industries use them to encode standards from their architecture review board.
Why patterns exist
Once you have more than a handful of repos under governance, you discover that the same architecture rules keep showing up:
- Every public-facing service must sit behind an API gateway.
- Every database holding PII must declare an
encryption-at-restcontrol. - Every async worker must consume from exactly one queue and publish to none.
Without patterns, you'd duplicate these rules in every repo's CALM file (and watch them drift). A pattern hoists the rule into one JSON Schema file that every architecture references. Change the pattern, every repo that references it re-validates against the new rule on its next PR.
Anatomy of a pattern
A CALM pattern is a standard JSON Schema document, identified by two markers in its
head — a $schema pointing at JSON Schema, and a $id
or referenced schema pointing at CALM. The body uses standard
properties, required, allOf, etc., to
constrain the shape of a CALM architecture document that claims to be an instance
of this pattern.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://patterns.myorg.io/public-service-behind-gateway.json",
"title": "Public service behind API gateway",
"description": "Any service exposed to the public internet must sit behind an API gateway node.",
"type": "object",
"required": ["nodes", "relationships"],
"properties": {
"nodes": {
"type": "array",
"contains": {
// Must contain at least one node of type "api-gateway"
"properties": {
"node-type": { "const": "api-gateway" }
},
"required": ["node-type"]
}
}
}
}
That's a complete pattern. Save it as
architecture/patterns/public-service-behind-gateway.json in any repo
(or host it at a URL all your repos can fetch — same effect via federation).
Applying a pattern to your architecture
A CALM architecture claims conformance to a pattern by setting its
$schema field to the pattern's URL:
{
"$schema": "https://patterns.myorg.io/public-service-behind-gateway.json",
"name": "Public Storefront",
"nodes": [
{ "unique-id": "storefront-gw", "node-type": "api-gateway", "name": "Storefront Gateway" },
{ "unique-id": "storefront", "node-type": "service", "name": "Storefront API" }
],
"relationships": [ /* ... */ ]
}
At validation time, ArchRails fetches the schema referenced by $schema,
classifies the file as a pattern instance, and runs JSON Schema validation. A
missing api-gateway node fails the PR with a schema violation pointing
at the missing requirement.
$schema + $id markers in the head of every
*.json file in your repo. Pattern files are routed to the pattern
validator; architecture files referencing a pattern URL are validated against it.
You don't need to register patterns anywhere — the references do the wiring.
Common shapes
A starting set most regulated-industry tenants land on:
- Public service behind gateway — the example above. Useful anywhere you have a public-facing surface that needs WAF/auth/rate-limiting before hitting application code.
-
PII database — requires a
data-classification: PIItag, anencryption-at-restcontrol, and that the database is deployed-in a network taggedtier: data. -
Fan-in queue worker — exactly one inbound
connectsfrom a queue, no outboundconnectsto user- facing systems. Common for batch settlement, reconciliation, async fraud scoring. - Three-tier web — web tier → app tier → data tier with no cross-tier shortcuts. The classic shape that drifts the fastest without enforcement.
- Hexagonal service — outbound calls go through a declared adapter interface, never directly to a third party. Prevents "we added a new vendor SDK three layers deep and forgot to declare it" drift.
Worked example · API gateway requirement
Suppose your security org requires every internet-facing service to sit behind a Cloudflare Worker. End-to-end:
1. Author the pattern at
architecture/patterns/internet-facing-cf-worker.json:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://patterns.myorg.io/internet-facing-cf-worker.json",
"title": "Internet-facing service behind Cloudflare Worker",
"type": "object",
"required": ["nodes", "relationships"],
"properties": {
"nodes": {
"type": "array",
"contains": {
"properties": {
"node-type": { "const": "cloudflare-worker" }
},
"required": ["node-type"]
}
},
"relationships": {
"type": "array",
"contains": {
// At least one connects edge into the worker from the internet actor.
"properties": {
"relationship-type": {
"properties": {
"connects": {
"properties": {
"source": { "properties": { "node": { "const": "internet" } } },
"destination": { "properties": { "node": { "const": "cf-worker" } } }
}
}
}
}
}
}
}
}
}
2. Reference it from the storefront repo's architecture:
{
"$schema": "https://patterns.myorg.io/internet-facing-cf-worker.json",
"name": "Storefront",
"nodes": [ /* must include cf-worker + storefront */ ],
"relationships": [ /* must include internet → cf-worker connects */ ]
}
3. Validate. The next PR against this repo runs the pattern check;
a storefront that drops the Cloudflare Worker fails CI with a JSON-Schema error
pointing at the missing contains. The security team sees the failure
in the PR review without having to remember to ask.
Versioning without breaking existing repos
A pattern is identified by its URL. If you change the URL, every architecture file referencing the old one is unaffected. The recommended pattern (no pun) is to version in the URL path:
https://patterns.myorg.io/internet-facing-cf-worker/v1.json https://patterns.myorg.io/internet-facing-cf-worker/v2.json
Tighten v2 (add a required rate-limit-interface, say). Repos that want
the stricter rule update their $schema reference on their next PR;
repos that aren't ready stay on v1 and continue to validate. You can later sunset
v1 by making the URL 404, which causes references to fail-closed with a fetch error.
Common mistakes
I authored a pattern but ArchRails treats it as an architecture file.
The classifier needs BOTH a JSON-Schema marker ($schema pointing at
a JSON Schema draft URL) AND a CALM marker somewhere in the head — usually
a CALM-hosted $id. If your $id is at your own domain,
add a top-level comment block or a CALM-hosted $ref so the
classifier picks up the marker. Without both, the file falls into
unknown and gets skipped.
The pattern URL 404s — what happens to the architecture file?
Fail-closed. ArchRails caches fetched schemas within a Lambda invocation but
won't fall back to a "missing schema" pass. The validator surfaces a
CALM_CONTROL_FETCH_ERROR warning and the dependent architecture
validation is skipped — which means latent drift will go uncaught. Treat
pattern URLs as production-critical artifacts; host them on infrastructure with
an uptime SLA at least as strong as your CI.
My pattern is too strict — every PR fails.
Usually means the pattern encoded a rule that's only true for some
instances. Two options. Loosen it: turn the strict
contains into a conditional using if/then (only
require the gateway when a public-internet edge exists). Split it:
publish a second pattern that's a different name and let architectures opt in
explicitly via $schema. Don't disable enforcement just to ship a
PR — you'll forget to re-enable it.
Can I reference more than one pattern from a single architecture file?
JSON Schema's $schema field accepts only one URL, but you can
compose by authoring a meta-pattern that uses allOf to require
conformance to several constituent patterns. Reference the meta-pattern from
your architecture and you get the composed rule set in one shot.