Link Types and Relationships

Connect your object types into a graph. One-to-many, many-to-many, intersection links, cardinality, and the rules that keep relationships honest.

⚡ intermediate
⏱️ 75 minutes
👤 SuperML Team

· Ontology · 7 min read

📋 Prerequisites

  • Lesson 5: Property Types and Data Types

🎯 What You'll Learn

  • Define link types with explicit cardinality and direction
  • Model one-to-many, many-to-many, and intersection-table relationships
  • Decide between a link type and a property when modeling relationships
  • Avoid common link-modeling pitfalls

If object types are the nouns of your ontology, link types are the verbs that connect them.

A Customer does not exist in isolation. They place orders. They signed contracts. They belong to a region. Each of those verbs is a link type.

A link type definition includes:

  • The two object types it connects.
  • The direction(s) the link is navigable.
  • The cardinality on each end.
  • A display name for each direction (placed orders / placed by).
  • Optional link properties — facts about the relationship itself, not the entities.

A first example

linkType: customerPlacedOrder
fromObjectType: Customer
toObjectType:   Order
fromDisplayName: placed orders
toDisplayName:   placed by
cardinality:    oneToMany    # one Customer → many Orders, each Order → one Customer

Now customer.placedOrders gives you all the Orders, and order.placedBy gives you the Customer.

Cardinality

Cardinality describes how many objects can be linked on each side. The standard cases:

One-to-one (1:1)

A Customer has exactly one CustomerAccount, and an Account belongs to exactly one Customer.

cardinality: oneToOne

Rare in practice. Often a sign the two should be a single object type. Use it deliberately — common legitimate use: separating sensitive data behind tighter security.

One-to-many (1:N)

The most common case. Customer → Orders, Order → OrderLines, Hub → Shipments.

cardinality: oneToMany

The “many” side stores the foreign key in its backing datasource (Order.customerId).

Many-to-many (N:M)

A Driver can operate many Vehicles, and a Vehicle can be operated by many Drivers.

linkType: driverOperatesVehicle
fromObjectType: Driver
toObjectType:   Vehicle
cardinality:    manyToMany

Many-to-many is where models start to wobble. The big question: does the relationship itself have properties?

  • “Alice drives the truck” → no extra facts. A pure many-to-many link is fine.
  • “Alice has been certified to drive this truck since Jan 2024 and her certification expires in Dec 2026” → those facts belong on the relationship.

When the relationship carries facts, promote it to an intersection object type:

objectType: DriverCertification
properties:
  - { name: driverCertificationId, type: string, primaryKey: true }
  - { name: certifiedAt, type: timestamp }
  - { name: expiresAt,   type: timestamp }
  - { name: status, type: enum<CertificationStatus> }

# Now two one-to-many links:
linkType: driverCertificationOfDriver
fromObjectType: Driver
toObjectType:   DriverCertification
cardinality:    oneToMany

linkType: driverCertificationForVehicle
fromObjectType: Vehicle
toObjectType:   DriverCertification
cardinality:    oneToMany

The DriverCertification is the intersection — it is a real entity in your business, not a hidden join table.

Direction and navigability

Some platforms support directional vs bidirectional links. A link type definition typically specifies both display names:

  • Customer → placed orders → Order (forward)
  • Order → placed by → Customer (reverse)

Both directions should be navigable in code:

const orders = await customer.placedOrders.load();
const buyer  = await order.placedBy.load();

If a direction is meaningless or never used, you can omit a display name and skip the reverse navigation — but be cautious. Today’s “never used” is tomorrow’s required query.

Sometimes a relationship can be modeled either as a link type or as a foreign-key property on the object. When to choose which:

Use a link type whenUse a property when
You want graph traversal in queriesThe target is referenced but not navigated
The target is a first-class entity in the ontologyThe target is a value, not an entity (currency code)
You will need to traverse links repeatedly (customer → orders → lines)The reference is for join tracking only
Either side may need link properties laterThe relationship is intrinsic to the value

Reasonable defaults:

  • Order.customerId: stringalso register a link type Order → placedBy → Customer. Both are useful.
  • Address.country: enum<CountryCode> — just a property, not a link to Country (countries aren’t a first-class entity in your domain).
  • Shipment.currentDriverId: string — definitely a link too.

The link type does not replace the property; it makes the relationship navigable at the ontology level.

When the link itself has facts, but you do not want a full intersection object type, some platforms support link properties on the link directly:

linkType: driverOperatesVehicle
fromObjectType: Driver
toObjectType:   Vehicle
cardinality:    manyToMany
linkProperties:
  - { name: assignedAt,  type: timestamp }
  - { name: assignedBy,  type: string }     # user who made the assignment

This is a compromise — lighter than an intersection object type, but the link properties cannot themselves be linked to other objects. Use it for short-lived metadata; promote to an intersection object type when the relationship grows in importance.

Cardinality at scale — watch the fan-out

A Customer linked to 10 orders is normal. A Customer linked to 10 million orders (long-tail B2C, or a “system” pseudo-customer) is a problem:

  • Loading all linked orders becomes infeasible.
  • Indexing slows down.
  • UIs that “show all linked objects” hang.

Mitigations:

  1. Default to paginated traversal. Never assume all links fit in memory.
  2. Detect “hub” objects — a small number of object instances with massive fan-out — and treat them carefully.
  3. Consider summary properties. Store customer.totalOrderCount and customer.lastOrderId as derived properties instead of always loading all orders.

A worked example — the logistics graph

Let’s draw out the link structure of our running example:

Customer ─placedOrder→ Order

                          └─containsLine→ OrderLine

                                              └─producedShipment→ Shipment

                                            ┌─originatesFrom→ Hub  ←──┘
                                            │  destination →  Hub  ←──┘
                                            ├─assignedTo →   Driver
                                            └─carriedBy →    Vehicle

Driver ─operates← (DriverAssignment) →operates→ Vehicle

                          (with: assignedAt, assignedBy, status)

Notice:

  • Cardinalities are made explicit: Customer 1:N Order, Order 1:N OrderLine, Driver N:M Vehicle (through DriverAssignment).
  • Hubs appear on both ends of Shipment — origin and destination are two different link types pointing to the same object type.
  • DriverAssignment is an intersection object type with its own properties — because the certification, assignment history, and current status matter on their own.

Common pitfalls

Pitfall 1 — Conflating identity and links. Don’t have Customer.companyName: string and Order.customerCompanyName: string. Pick one source of truth (Customer.companyName), navigate via the link type when you need it on Order.

Pitfall 2 — Modeling enums as links. A property Customer.tier: enum<CustomerTier> does not need to be a link to a CustomerTier object type unless the tiers themselves have rich behavior (rules, benefits, expirations). Most enums stay enums.

Pitfall 3 — Forgetting reverse navigability. You define Order → placedBy → Customer but forget to label the reverse Customer → placedOrders. Now customer.placedOrders is awkward to express. Define both display names up front.

Pitfall 4 — Many-to-many without intersection objects when there are facts. Eventually someone asks “when was this driver assigned to this vehicle?” If you stored a pure many-to-many link, you have no answer. Use an intersection object type from the start when there is even a hint of relationship facts.

Pitfall 5 — Circular link soup. If you can navigate A → B → C → A → B → ... and it makes sense in your data, you have a cycle. Cycles are fine in the model, but queries that walk them without termination conditions will run forever. Always bound traversal depth.

Modeling cheat sheet

Q: Are both ends real entities in our business?
   → No  → property (foreign-key reference)
   → Yes → continue

Q: Will we navigate this relationship?
   → No  → property is enough
   → Yes → link type

Q: Does the relationship itself carry facts?
   → No  → simple link type
   → Yes, lightweight → link with link properties
   → Yes, rich  → intersection object type + two link types

Q: What is the cardinality?
   → Pick one: 1:1, 1:N, N:M
   → Sanity-check: any side ever going to fan out into millions?
       → Yes → plan for pagination + summary properties

Key takeaways

  • Link types model the verbs between object types — typed, directional, with explicit cardinality.
  • One-to-many is the bread-and-butter case; many-to-many usually becomes an intersection object type.
  • Link types and properties can coexist — keep the foreign-key property for raw joins, add the link type for ontology-level traversal.
  • Watch for fan-out: a small number of objects with millions of links is a special case to plan for, not an edge case to discover in production.

What’s next

Reads are now expressive — you can traverse the graph. Next we cover writes: the typed, validated, auditable way to mutate the ontology — action types.


The graph is wired. Now we make it move. 🔗

Related Tutorials

⚡intermediate ⏱️ 120 minutes

Building Object Types and Links

Hands-on: take the Northwind logistics model from design to code. Object types, enums, structs, link types, and a working multi-entity ontology.

Ontology8 min read
ontologyimplementationhands-on +2
⚡intermediate ⏱️ 45 minutes

Introduction to the Ontology

Why ontologies exist, what problems they solve, and where they fit between raw data and the applications that depend on it.

Ontology4 min read
ontologysemantic layerdata modeling +1
⚡intermediate ⏱️ 75 minutes

Object Types

Object types are the nouns of your ontology. Learn how to define them: primary keys, titles, descriptions, properties, and the common pitfalls that ruin a model later.

Ontology6 min read
ontologyobject typesdata modeling
⚡intermediate ⏱️ 75 minutes

Property Types and Data Types

The type system at the heart of the ontology — primitives, semantic types, enums, structs, arrays, geo, attachments — and how to design properties that scale.

Ontology6 min read
ontologyproperty typesdata modeling +1