Course Content
Link Types and Relationships
Model the verbs between objects: one-to-many, many-to-many, intersection links, and cardinality
What a link type is
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 CustomerNow 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: oneToOneRare 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: oneToManyThe “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: manyToManyMany-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: oneToManyThe 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.
Link type or property? A decision rule
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 when | Use a property when |
|---|---|
| You want graph traversal in queries | The target is referenced but not navigated |
| The target is a first-class entity in the ontology | The 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 later | The relationship is intrinsic to the value |
Reasonable defaults:
Order.customerId: string— also register a link typeOrder → placedBy → Customer. Both are useful.Address.country: enum<CountryCode>— just a property, not a link toCountry(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.
Link properties
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 assignmentThis 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:
- Default to paginated traversal. Never assume all links fit in memory.
- Detect “hub” objects — a small number of object instances with massive fan-out — and treat them carefully.
- Consider summary properties. Store
customer.totalOrderCountandcustomer.lastOrderIdas 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(throughDriverAssignment). - 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 propertiesKey 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. 🔗