Lesson 4 of 6 · 8 min read · Governance

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.

In this lesson
  1. Why patterns exist
  2. Anatomy of a pattern
  3. Applying a pattern to your architecture
  4. Common shapes
  5. Worked example · API gateway requirement
  6. Versioning without breaking existing repos
  7. Common mistakes

Why patterns exist

Once you have more than a handful of repos under governance, you discover that the same architecture rules keep showing up:

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.

ArchRails detects patterns automatically. The file classifier reads the $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:

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.