logo
28 Jun 2026

The Ontology, Part 4: What Objects, Properties, and Links Really Are

Part 4 of the beginner's guide to ontologies. We've used objects, properties, and links since part 1 — now we look at the grammar underneath them: what gives an object its identity, what a property's type really buys you, and the one word nobody defined — link cardinality, including the many-to-many that hides its own data.

This is part 4. We've used objects, properties, and links since part 1, reused them in part 2, and queried them in part 3 — all while saying "an object has properties and links" as if that were obvious. It isn't. There's real grammar underneath, and it's the grammar everything else stands on. So let's slow down and look at the three building blocks up close: what gives an object its identity, what a property's type really buys you, and the one word nobody defined — link cardinality.


First, what makes a thing that thing?

Start with objects. Part 1 said objects are the nouns — Customer, Order, Product. Fine. But here's a question that sounds dumb and isn't: if two customers are both named "Sara Ahmed," are they the same customer?

Obviously not. But how does the system know? Not by the name — names collide, and names change (people marry, fix typos, switch "Mohammed" to "Mohamed"). The thing that says "this is this customer, not that one" is the object's identity — a stable key, usually called the primary key.

Sara Ahmed id: cust_8842 Sara Ahmed id: cust_3190 Same name. Different people. Order #5567 customer → cust_3190 The link points at the key — never the name.

Think of it like a passport number. Your name is printed on the passport, but the number is what's unique — two people can share a name, never a passport number. In Part 1's mapping step, this was the quiet line "which column uniquely identifies one Customer." That column is the object's identity.

Identity is load-bearing for the whole system, in three places:

  • Links point at identity, not at names. When an Order links to its Customer, it stores the customer's key (cust_3190), not the string "Sara Ahmed." That's why renaming a customer never breaks their orders.
  • Edits are keyed by identity. Remember the writeback store from Part 1? An edit to a customer is filed under that key, so it lands on the right object and nowhere else.
  • Deduplication needs it. If two messy source rows are really the same person, the mapping has to resolve them to one key — or you get two ghost customers.

👩‍💻 Mai (junior): So why not just key on email? Everyone has one.

👨‍🏫 Hamed (mentor): Until they change it. The golden rule of a primary key: pick something that never changes and never repeats. Email fails the first test; names fail both. When nothing natural fits, you mint a meaningless id — cust_3190because it has no meaning to drift. Boring and permanent beats descriptive and fragile.

Properties have types — and the type is a promise

Part 1 drew properties as the italic text inside the boxes — name, total, date. What it skipped is that every property has a type, and the type is the most underrated decision in the whole model. A type isn't just "how it's stored." It's a promise the rest of the system gets to rely on.

Here's the menu, grouped the way you'd really reach for it:

Basics Structured Specialized text number true / false date / timestamp enum (a fixed set) array (many values) struct (a bundle) location (geo) time-series attachment The type decides what you can store, validate, filter, and how the UI renders it.
  • The basics: text, number, boolean, date/timestamp. The bread and butter.
  • Structured: an enum is a fixed set of allowed values (status is one of open / paid / cancelled — so nobody can typo opn). An array holds many values (a list of tags). A struct bundles a few fields into one property (an address of street/city/zip that travels together).
  • Specialized: a location (latitude/longitude) is what lets an object land on a map. A time-series holds readings over time (a sensor's temperature). An attachment points at a file.

Why does picking the right one matter so much? Because the type is what makes everything downstream smart, for free:

  • Type it as a date, and Part 3's query model can filter by range ("last 30 days") and sort chronologically. Store it as text and you've thrown that away.
  • Type it as a location, and a map component can drop a pin — this is exactly the Facility / ILocatable idea from Part 2. That interface only works because location is a real geo type, not a string.
  • Type it as an enum, and the UI renders a clean dropdown, the data can't hold a misspelling, and a chart can group by it without surprises.
type Order = {
  id: string;                              // identity — a text key
  total: number;                           // money — filter by >, sort, sum
  status: "open" | "paid" | "cancelled";   // enum — a typo can't exist
  createdAt: Date;                         // date — range filters, chronological sort
  shipTo: Address;                         // struct — a bundle that travels together
  tags: string[];                          // array — many values
};

👩‍💻 Mai: Couldn't I just store all of it as text and parse it when I need to?

👨‍🏫 Hamed: You could — and you'd spend the rest of the project paying for it. "Stringly-typed" data can't be range-filtered, can't be validated, can't be mapped, can't be charted without re-parsing every single time. The type is a one-time decision that buys correctness everywhere after. It's the difference between the system knowing what it holds and merely storing it.

One callback: not every property is stored. Some are derived — computed by a function, like Part 1's risk score or a daysOpen. They still have a type, and Part 3 lets you select, filter, and sort on them exactly like stored ones.

Now the big one — the idea the series has been quietly leaning on without ever naming. Part 1 told you links are relationships ("a Customer places an Order"). What it never said is that every link has a cardinality: how many things sit on each end. There are three shapes, and you already know all of them from real life.

Person Passport one ↔ one each person has exactly one passport, and the reverse Customer Orders one → many one customer, many orders — but each order has exactly one customer Students Courses many ↔ many a student takes many courses; a course has many students
  • One-to-one. A Person and their Passport — one each way. Rare in practice, and usually a sign that two things could have been one object, or that you split off sensitive fields on purpose.
  • One-to-many. A Customer and their Orders. One customer has many orders; each order belongs to exactly one customer. This is the workhorse — most links you draw are one-to-many. (Part 1's "a Dog has exactly one Owner" is this same link, seen from the Dog's side.)
  • Many-to-many. Students and Courses. A student takes many courses; a course enrolls many students. Both ends are "many."

👩‍💻 Mai: Wait — go back to Part 1. "An Order contains Products." A product can be in lots of orders, and an order has lots of products. Isn't that... many-to-many?

👨‍🏫 Hamed: It is — and you just caught the most common modeling slip in the book. We drew it as a plain arrow, but it's a many-to-many. Spotting it early matters, because many-to-many has a twist the other two don't.

Here's the twist. In a one-to-many, the link is just a pointer. But a many-to-many often has facts that belong to the relationship itself, not to either end. Take Student–Course: the grade and the enrollment date aren't a fact about the student (they have many grades) or about the course (it has many) — they're a fact about that student in that course. When that happens, the link grows up into its own object — an Enrollment — sitting between the two:

// Many-to-many with no extra facts → a plain link:
type Student = { id: string; courses: Course[] };

// Many-to-many that carries its own data → promote it to an object:
type Enrollment = {
  id: string;
  student: Student;   // to-one
  course: Course;     // to-one
  grade: string;      // a fact about the pairing itself
  enrolledAt: Date;
};

That's the whole trick: a many-to-many with its own properties becomes an object in the middle, linked one-to-many to each side. Once you see it, you see it everywhere — orders and products (the middle object is the line item, holding quantity and price), users and roles, doctors and patients.

And cardinality isn't trivia — it decides how you use the link. Traversing the "many" side (Part 3's .traverse("orders")) hands you a set you page through; traversing the "one" side hands you a single object. It even decides the UI: a to-many link renders as a table or list; a to-one renders as a single linked card.

👩‍💻 Mai: So when I built that object set in Part 3 and followed a link, the cardinality is why I got back a list I could keep filtering?

👨‍🏫 Hamed: Exactly. "Traversal in, set out" only makes sense because you followed a to-many link. You were leaning on cardinality the whole time — now you know its name.


The one-paragraph summary

Under the friendly boxes-and-arrows, the three building blocks have real grammar. An object is pinned down by a stable identity — a primary key that never changes and never repeats — and that's what links point at and what edits are filed under. A property has a type, and the type is a promise: choose date, enum, or location and you get range filters, validation, and maps for free; choose text for everything and you pay for it forever. A link has a cardinality — one-to-one, one-to-many, or many-to-many — and the moment a many-to-many carries its own facts, it quietly becomes an object in the middle. Identity, types, cardinality. Get these three right and everything you've read so far — the query model, the actions, the SDK, the security gates — has solid ground to stand on. These are the bricks; the rest of the ontology is what you build with them.


Missed the earlier parts? Start with part 1 for the big picture, part 2 for reuse and agents, and part 3 for how you read the model. Next up: now that the nouns are precise, the other half of part 3's story — not how you read the ontology, but how you safely change it.