Lesson 3 of 6 · 7 min read · Foundations

Declare relationships

Relationships are how nodes interact — one service calls another, a service is deployed in a network, an actor uses a system. CALM 1.0 gives you four relationship types; picking the right one matters because ArchRails enforces different rules for each.

In this lesson
  1. Anatomy of a relationship
  2. connects · service-to-service
  3. interacts · actor-to-system
  4. composed-of · grouping
  5. deployed-in · runtime topology
  6. Picking the right type
  7. Cross-repo relationships
  8. What ArchRails enforces

Anatomy of a relationship

Every relationship lives in the top-level relationships array of a CALM file and has the same outer shape:

{
  "unique-id":        "payment-to-card-data",
  "relationship-type": { /* one of: connects, interacts, composed-of, deployed-in */ },
  "protocol":         "https",            // optional, mostly used with connects
  "metadata":         { "port": 443 }   // optional, free-form annotations
}

The relationship-type body is what changes between the four types — each has its own shape. Walk through each one below.

connects · service-to-service

The workhorse. Use connects any time one node makes a protocol-level call to another — HTTP, gRPC, TCP, AMQP, anything wire-level.

{
  "unique-id": "payment-to-card-data",
  "relationship-type": {
    "connects": {
      "source":      { "node": "payment-api" },
      "destination": { "node": "card-data-service",
                        "interfaces": ["card-data-http"] }
    }
  },
  "protocol": "https",
  "metadata": { "port": 443 }
}

Two important details:

interacts · actor-to-system

Used when a human actor is involved. The shape differs from connects — one actor on the left, a list of nodes on the right.

{
  "unique-id": "trader-uses-web-gui",
  "relationship-type": {
    "interacts": {
      "actor": "trader",
      "nodes": ["web-gui"]
    }
  }
}

nodes is plural and accepts multiple targets — one interacts relationship can express "the support agent uses both the CRM and the case management tool." The validator fans these out into one edge per target.

composed-of · grouping

Used when one node is logically made up of other nodes — "the trading platform consists of these microservices." Helps the dashboard cluster related nodes and lets you write patterns that target a system without enumerating every child.

{
  "unique-id": "trading-platform-composition",
  "relationship-type": {
    "composed-of": {
      "container": "trading-platform",
      "nodes":     ["orders-server", "risk-engine", "market-data"]
    }
  }
}
One container per node. ArchRails flags a node that appears under two different composed-of parents — the model becomes ambiguous ("which platform owns this?"). If a service genuinely belongs to two systems, you probably want to model it as two services with a connects between them.

deployed-in · runtime topology

Used when one node lives inside another at runtime — a service running in a Kubernetes cluster, a database in a VPC, a Lambda in an AWS account. The container is typically a network or system; the deployed children are services and databases.

{
  "unique-id": "prod-vpc-deployment",
  "relationship-type": {
    "deployed-in": {
      "container": "prod-vpc",
      "nodes":     ["orders-server", "orders-db", "orders-cache"]
    }
  }
}

Same one-container-per-node rule applies. ArchRails will flag a service declared as deployed in two different containers — runtime topology has to be unambiguous for controls (e.g., "must be deployed in a PCI-scoped VPC") to make sense.

Picking the right type

A 30-second decision tree:

Is one end of the relationship a person?               → interacts
Is the relationship "lives inside at runtime"?         → deployed-in
Is the relationship "is logically made up of"?         → composed-of
Otherwise — it's a wire-level call between two systems  → connects

Common mistakes:

Cross-repo relationships

Most production setups have services in different repos. ArchRails resolves cross-repo references via federation — each repo bootstraps its own CALM file, and the federated graph stitches them together at the boundary.

Two valid ways to mark a reference as cross-repo. Both work; the resolver checks both and endpoint-level wins when both are present (more specific, and aligned with the FINOS contribution).

Endpoint-level (recommended): declare the repo directly on the relationship endpoint. The foreign node doesn't need to be declared locally — the endpoint says everything the resolver needs.

{
  "unique-id": "web-gui-to-account-service",
  "relationship-type": {
    "connects": {
      "source":      { "node": "web-gui" },
      "destination": {
        "node":       "account-service",
        "repository": "https://github.com/myorg/accounts-server"
      }
    }
  }
}

This is the ArchRails-proposed FINOS extension on the relationship-endpoint shape. Two things to like about it: it keeps the cross-repo info next to the edge that uses it (no need to redeclare the foreign node), and it's the natural fit for "contract" repos that exist purely to declare cross-service edges.

Node-level (still supported — monorepo legacy convention): mark the foreign node's repo on the node spec itself, using metadata.repository:

// In the frontend repo's CALM doc — declaring a node that lives elsewhere:
{
  "unique-id": "account-service",
  "node-type": "service",
  "name":      "Account Service",
  "description": "Authoritative account ledger. Lives in accounts-server repo.",
  "metadata": {
    "repository": "https://github.com/myorg/accounts-server"
  }
}

Useful when you've already got the foreign node declared locally for other reasons (you wanted its name and description to surface in the visualizer, for example). The relationship pointing at this node looks like any other connects — no extra field needed on the edge.

Forgetting the repository field is the #1 federation gotcha. Without it, the resolver treats the destination as a local node — and since you never declared it locally, it shows up as "unknown" in the federated graph view and cross-repo PRs aren't flagged. If federation looks broken, check this first.

What ArchRails enforces

At PR time, ArchRails uses your declared relationships as the allow-list for what the code is permitted to do: