---
type: Axis
title: cardinality (relationship-multiplicity axis)
description: The cardinality/relationship-multiplicity architecture axis — how many entities a relationship may connect (1:1 → 1:N → N:M). A per-entity-with-default axis whose commitment surface is the persistent-data schema (the FK / join-table shape). DEEP (RFC #75 5A.4 / P1 #851 → promoted 4.5.0a6 #856); worked example is the issue↔milestone (1:N FK, the #769 silent-milestone-drop) and issue↔label (N:M join) relationships.
tags: [axis, architecture, cardinality, relationship, multiplicity, join, rfc-75, deep]
timestamp: 2026-06-20T00:00:00Z
---

# Cardinality (relationship-multiplicity axis)

> **DEEP axis (P1 #851 → 4.5.0a6 #856).** Full §6 depth with a worked example
> from the skill's own history — the issue↔milestone 1:N FK (the #769
> create_issue silent-milestone-drop was a lost write to it) and the issue↔label
> N:M join, contrasted with the comma-list anti-pattern this axis forbids.
> Distinct from `schema-flexibility` (how much one entity's *shape* varies):
> cardinality is how many entities a *relationship between* them may connect.

## Concept

Cardinality is the **relationship-multiplicity** axis (RFC #75 §3): how many
entities a relationship may connect. The ladder runs 1:1 (each side relates to at
most one of the other) → 1:N (one parent, many children) → N:M (many-to-many,
requiring a join table). Its **commitment surface is the persistent-data schema**
(§4): the multiplicity is the FK placement / join-table shape, and widening it is
a structural migration. Its **scope is per-entity-with-default** (§5): a
system-wide default ("relationships are 1:N — a foreign key on the child") with
per-relationship deviations declared in the entity record.

## Why it is an axis (discriminator test — §2)

1. **Ordered tiers** — each rung admits more connections per side (1:1 ⊂ 1:N ⊂
   N:M).
2. **Cross-cutting bundle** — a rung dictates the FK/join-table schema, the
   referential-integrity rules, the query shape (a join vs a column), and the
   relationship tests.
3. **Asymmetric migration cost** — going 1:1 → 1:N → N:M means introducing/
   moving FKs and standing up a join table over existing rows; walking *back*
   means picking a winner and losing the surplus relationships (data loss).
4. **Requirement-keyed selection** — falsifiable probes pick the rung.

## Schema view (the §6 entry — validated by `axis_catalog`)

```yaml
id: cardinality
kind: architecture-axis
scope: per-entity-with-default
default_tier: one-to-many
maintenance_discipline: >-
  relationships are 1:N by default — a foreign key on the child pointing at the
  parent. A per-relationship deviation (1:1 via a unique constraint, or N:M via a
  join table) is declared in the entity record's axis-materialization facet.
  Widening cardinality is a structural migration that must preserve existing
  links; the discipline forbids a denormalized comma-list column standing in for
  a real N:M join table. Per-relationship deviations live in the entity record.
selection:
  probes:
    - "Does each side relate to AT MOST one of the other? → one-to-one (a unique FK)."
    - "Does one parent own MANY children, each with one parent? → one-to-many (FK on child)."
    - "Can each side relate to MANY of the other? → many-to-many (a join table)."
    - "Is a comma-separated column being used to fake a many-to-many? → it wants a join table."
composes_with:
  - "relationship integrity = cardinality × schema-evolution (a widening is a migration)"
applies_when: []
tiers:
  - tier: one-to-one
    commitment_delta:
      surface: persistent-data-schema
      delta: "each side relates to at most one of the other; enforced by a unique constraint on the foreign key. Often a candidate to fold into one table."
    admin_tool: "schema/constraint tool."
    enforcement:
      - "a unique constraint guarantees at most one link per side; a second link is rejected."
    tests:
      - "linking a second partner to a 1:1 side is rejected."
    forecloses: "more than one related entity per side."
    migration_cost_to_next: "low — drop the unique constraint to allow many children (1:N)."
  - tier: one-to-many
    commitment_delta:
      surface: persistent-data-schema
      delta: "one parent owns many children; a foreign key on the child points at the parent. The common default."
    admin_tool: "schema tool; an orphan-row report."
    enforcement:
      - "the child's FK references a valid parent; a delete of the parent cascades or is restricted per policy."
    tests:
      - "many children attach to one parent; a child cannot point at a nonexistent parent."
    forecloses: "a child belonging to more than one parent."
    migration_cost_to_next: "medium — introduce a join table to allow many parents (N:M)."
  - tier: many-to-many
    commitment_delta:
      surface: persistent-data-schema
      delta: "each side may relate to many of the other; a join table holds the pairs (optionally with link attributes). Queries join through it."
    admin_tool: "join-table editor; a dangling-pair audit."
    enforcement:
      - "each join row references two valid endpoints; duplicate pairs are prevented by a composite key."
    tests:
      - "an entity links to several on each side; a duplicate pair is rejected; deleting an endpoint cleans its pairs."
    forecloses: "single-FK simplicity; every traversal is now a join."
    migration_cost_to_next: "n/a — top of the ladder."
examples:
  - "The skill's own relationships span the ladder. issue↔milestone is ONE-TO-MANY: a milestone owns many issues, each issue carries exactly one milestone FK — and that single FK is load-bearing, because the #769 create_issue silent-milestone-drop was precisely a failed write to it (the issue landed with a NULL milestone, so milestone_coverage saw an orphan and the open-issue gate went vacuous, #715). issue↔label is MANY-TO-MANY: an issue carries many labels and a label tags many issues, so it is a real join — the textbook case where a denormalized comma-list `labels` column would be the anti-pattern this axis's discipline explicitly forbids (you cannot enforce referential integrity, dedupe pairs, or clean up an endpoint deletion against a string). plan_entries↔milestone is also 1:N with a hard FK whose violation (a poisoned projection breaking it) caused milestone_coverage to false-fail (#754) — the relationship-integrity scar. The skill has never needed to WALK BACK a cardinality (the data-loss direction); every widening it did (e.g. growing label sets) was the safe 1:1→1:N→N:M direction."
```

## Selection procedure (§8)

Bidirectional check per relationship under the 1:N default. The escalation pass
asks "can each side relate to many?" — a yes forces N:M (a real join table, not a
comma-list). Recorded in the entity record; a faked many-to-many is a
systems-analysis finding (5A.5).

## Composition (§5)

Cardinality × schema-evolution = **relationship integrity** — widening
cardinality is a migration that must preserve existing links. Rule authored in
5A.3.
