Q: Why relationship patterns ?
A: To explore and improve how we describe and build complex systems by focusing on relationships—moving beyond basic composition to more expressive ways of relating entities.
Relationships are hard. We all struggle to understand how to interpret them to make our lives easier.
Trying to make sense of how the world's things and our many ideas about it relate with one another, and impact our understanding likely began with Aristotle, as he categorized entities in hierarchical and compositional ways.
In his Categories and Metaphysics, Aristotle laid the foundations for understanding entities and their relationships. He introduced taxonomy (hierarchies of "isA") and distinguished between:
More explicit consideration of association and aggregation emerged with modern logic (19th century) and became formalized with database and programming models (20th century).
Here the focus is on relationships and how to describe and build complex systems by creating and relying on more expressive relations.
Different programming models think about relationships in different ways. For example, most programming languages use composition to define the system structure and behaviour, whether it's Object, Function or Module composition. Component based architectures, Relational and Document databases, HTML, frameworks like React, and many other technologies use composition. Composition is pervasive when thinking about building systems.
The list below shows the relation types we aim to support.
| Concept | Short Form | Description |
|---|---|---|
| Taxonomic | ||
| Inheritance | isA | Hierarchical specialization of entities. A sub-entity type is a specialized entity type, and inherits what its parents have. |
| Instantiation | isAnActual, isAnInstanceOf | Links an actual entity to its type: MyRAV4 isAnInstanceOf Car. A special kind of isA - the end of the line, nothing specializes an instance. |
| Refinement | <the same relation, closer> | A sub-type narrows what it inherits: the most specific fact wins. To-one replaces, to-many adds. |
| Structural | ||
| Composition | has, hasOne, hasMany | Entity has, or is made of, other entities: its fields and parts. The parent of all the structural relations below. |
| Ownership | hasOwn | Strong ownership; the part's lifecycle is tied to the whole. The part belongs to this entity only, and cannot be transferred to another. |
| Aggregation | hasPart, contains | Weak ownership. Entity contains part(s) which exist independently, and can change where they belong over time. |
| Association | hasOneRef, hasManyRefs | Entity refers to one or more independent entities, which can be referred to by other entities at the same time. Loose connection; no ownership or lifecycle implications. |
| Role Types | <role> isA <type> | Two fields of the same type, each with its own role: ShipAddress and BillAddress are both Addresses. (being designed) |
| Behavioral | ||
| Dependency | uses | Represents functional reliance. Entity uses one or more other entities to do what it does. |
| Requirement | requires | A field every actual entity of this type should fill in: Car requires vin. Inherited by sub-types. |
| Complex | ||
| Custom Relation | <custom> | Relations are data too: a new relation is a specialization of an existing one (contains isA hasPart) and behaves like it. Relation groups work this way already (a kind of mappingRelation is handled as one); any relation next. |
| Conditional Relation | <any> | Relation only applies when a condition is true: a fact about a fact. (planned) |
| Managed Relation | managesRelation | Facts along the managed relation are generated instead of written by hand. |
| Taxonomic Composite | managesCompositionFor | A taxonomy models hierarchical relationships, while a composite pattern manages "whole-part" relationships. Here the two are kept in sync. |
| Filtering entities | filters | An entity whose members are computed from a criteria over another one, not stated. (being designed) |
| Composite Facts | Facts composed from basic facts. (planned) | |
| Bitemporal Facts | from, to | When a fact was true in the world, and when it was recorded - kept apart. (history built, validity planned) |
| Data Aggregation | groupBy | Aggregates the members of a kind across dimensions taken from the model. (first version built) |
| Code Extensibility | code | Rules and calculations as approved expressions; code only where they can't reach. (rules built) |
| I/O relation | io | Other systems' data described by facts and read where it lives. (databases built) |
| System relation | system | Integrating with the platform's systems: authorization and actions as facts. (partly built) |
There is no alias relation. Other names for the same thing - a synonym, a translation, what one team calls a "unit" and another a "vehicle" - are entries in a dictionary, found by search, not separate entities to keep equal.
A sub-type inherits what its parents have, and can narrow it. Car hasOne Color; a ToyotaCar hasOne ToyotaColor; a ToyotaRAV4 hasOne ToyotaRAV4Colors; this RAV4 hasOne CrimsonRed. Each fact is a narrower version of the one above it, and the most specific one wins: a ToyotaRAV4 offers only its own colors, and this RAV4 is red.
For a single value (to-one) the closer fact replaces the ones above it - but they stay stored, inactive, so removing the closer fact falls back to the next one: delete CrimsonRed and this RAV4 again offers the RAV4 colors. For a collection (to-many) nothing is replaced: a Building hasMany Room, a House hasMany Kitchen and Bedroom, and a house still has room for a library. The collection stays open, and the narrower kinds show next to it.
The result must not depend on the order facts were written in or the path they were inherited through, so it's recomputed for every entity a change touches, and rebuilding it from scratch changes nothing. With multiple inheritance two parents can narrow the same field in different ways; that's a conflict, shown with both reasons, not settled by picking one.
Related: covariant overriding in programming languages; restrictions on a property in OWL (allValuesFrom); narrowing a property's range in a subclass, as in SHACL shapes per class.
The line between a type and a thing is blurry on purpose. Red isA Color reads as a value today, and tomorrow it's the type of CrimsonRed and ScarletRed. A ToyotaRAV4 is an instance of a car model in a catalog and a type for the cars a fleet owns. So the same relation, isA, does both, and anything can become a kind by having something based on it.
Only the end of the line is marked: MyRAV4 isAnInstanceOf ToyotaRAV4 says nothing will be based on MyRAV4. It inherits like isA, but nothing can specialize it, and something that already has sub-types can't become an instance. Records read from another database are instances: order 10248 can't be sub-typed.
Related: multi-level modelling, where a type can be an instance of a higher-level type (clabjects and potency - C. Atkinson, T. Kühne, "The Essence of Multilevel Metamodeling", UML 2001); power types and Type Object (in the Taxonomic Composite below); "punning" in OWL 2, which allows one name as class and individual but connects nothing between the two uses.
An order has two addresses: where it ships and where it's billed. Both are Addresses, and refinement would read the second as a narrower version of the first. A role is a sub-type that says which one: ShipAddress isA Address, BillAddress isA Address; Order hasOne ShipAddress, hasOne BillAddress. Each role is its own field, narrowed on its own. Values work the same way: seats isA unsignedInteger, odometerKm isA distance.
Between entities a role is a relation rather than a type: a vehicle drivenBy an Employee and assignedBy an Employee. Each is a kind of hasOneRef - it behaves like its parent, but is a field of its own. (being designed: relations defined as data, so a new one needs no code)
Related: Role Object (D. Bäumer, D. Riehle, W. Siberski, M. Wulf, PLoP 1997); M. Fowler, "Dealing with Roles" (PLoP 1997); sub-properties in RDF (rdfs:subPropertyOf); role names on UML associations.
A relation that manages another one: the facts along the managed relation are generated instead of written by hand, and kept consistent as things change. The Taxonomic Composite below is the first of them.
Generated facts are stored but derived, like a materialized view: the source (here the containment and a seed) is the truth, and what's generated from it is rebuilt when the source changes. Stored, they are ordinary facts - searchable, inheritable, authorized, exported like any other - and reads stay simple. Derived, they can't drift: nothing writes them by hand, and rebuilding from the same source gives the same result. The facts people add to a generated entity (a rule on Canada_ON_City) are theirs, and survive a rebuild.
The same idea already runs underneath everything: what an entity inherits through isA is computed when a fact is written and stored with it, and rebuilding it from the facts must change nothing.
A taxonomy models hierarchical relationships, while a composite pattern manages "whole-part" relationships. This term highlights the structure's alignment with classification systems and composition.
This is a recursive Dual-Hierarchy Composite-Inheritance Pattern describing a Multi-Dimensional Ontological Model of 2 hierarchies:
Each level builds on the previous one, and the entities in both the inheritance and composition hierarchies follow a consistent progression.
The goal here is to simplify the management of this data, by inferring inheritance from the composition data and keep the data in sync across the two relations between the two hierarchies.
Given a hierarchical type system describing Administrative Regions, a lot of types would need to be generated to support the creation of entities.
Below it shows the composition hierarchy. This hierarchy's data obviously can be derived from the inheritance hierarchy's data. When creating a new city of a certain type, it can be safely placed under that type's associated entity.
Canada_ON_Hamilton = Canada_ON_City.new(Hamilton) => Canada_ON_Hamilton.isA(Canada_ON_City) && Canada_ON.has(Canada_ON_Hamilton).
Creating an entity also generates its scoped types: one per level that can be below it, "a <level> within <entity>", a sub-type of the same level's type in the entity's container (Canada_ON_City isA Canada_City isA City). They are where facts about that level in that place go (what an Ontario city has that a BC city doesn't). The scoped types of the levels an entity contains directly are its templates: the only types that can be created in it. So a type always knows its container, the composition holds entities only, and a level can be skipped where the seed allows it: a Country has StateOrProvinces or Cities, so England (a Country of the UK) has its cities directly (UK_England_City), while UK_City.new(Leeds) is refused - a Kingdom contains Countries only.
Moving an entity to another container changes its type as well (a city moving to another province becomes a city of that province), and the scoped types below it move with it: the two hierarchies stay in sync. The names here are paths for readability; the identity of an entity doesn't depend on them.
The mechanism is not specific to regions: it applies wherever a containment has levels - a site has buildings, a building has floors, a floor has rooms; an organization has divisions, a division has departments.
Stored or inferred? The scoped types could be computed when read instead of stored: no facts until a type is used, a move is a single change, a new level applies at once. But a type that only exists while it's read has no identity to hang facts on, and every reader - browsing, exports, authorization - would have to learn about virtual types. So: stored but derived, as a Managed Relation above - generated from the containment and the seed, regenerated when either changes.
The AdministrativeRegion entity below has 4 sub-types:
Each specifies its direct children in the composition relation.
It also specifies which entity it manages the composition for (managesCompositionFor), and the levels that entity contains directly.
Running the script below generates the extensive type system and the composition hierarchy.
An entity defined by a criteria over another one: RedCar filters Car where color is Red. Its members are computed, not stated, so a car that is repainted leaves it on its own.
It sits between the two ways an entity gets members today. isA is stated: MyRAV4 isAnInstanceOf ToyotaRAV4 because someone said so, and it stays so until someone says otherwise. A query is computed but anonymous: a report's filters, a list's "In workshop" button. A filtering entity is a query with a name and an identity - so it can be reused (a list, a report's rows, the choices a form offers), described, and reasoned about.
The criteria are the ones reports already use: kinds below the filtered one, members of a choice or a list (and their kinds: color is a kind of Red), numbers, dates and text compared. Stored as facts, like a saved report's filters, so renaming a field doesn't break it.
Being designed: whether a filtering entity is only a view, or also a kind that can carry facts of its own (a RedCar requires a paint warranty - then a repaint changes what a car has); whether its members are computed when read or kept up to date on write (needed to say when something enters or leaves it: an alert); and whether access can be granted through it.
Related: defined classes in OWL (equivalentClass with restrictions), SHACL targets, database views, saved list views in business systems.
A fact composed from a path of basic facts: MyRAV4 has an owner, the owner has houses, so "the houses of MyRAV4's owner" can be asked for, and reasoned about, as one fact. (planned)
Composite facts are derived, never written: the path is the definition, and the result follows its basic facts. They show up already where a record read from another database meets the model: should a record show only its columns, or also what the model derives for it? A composite fact is the honest form - shown as derived, with the path it came from. Paths of one relation repeated (everything a region contains, at any depth: isPartOf+) are the simplest case.
Not to be confused with facts about facts (a fact that holds since 2007, a fact an import proposed): there the subject is a fact; here a fact is built from others.
Two kinds of time, kept apart. Valid time is when something is true in the world: Andrea owned MyRAV4 from April 2023 to June 2025. Recording time is when the system learned or changed it. Together they answer "who was the owner on January 1st, as we knew it last March?"
Recording time is a log: every change to a fact, an entity or a value is appended, one commit per action - who, when, on whose behalf - and never rewritten. Undo is a new commit with the inverse, so the log keeps both. Built.
Valid time goes on the fact: a period, from and to. Ending a relationship sets its end - the fact stays, so a car keeps its list of past owners. A to-one field over time means at most one at any moment, not one ever: periods may not overlap. "Current" is the facts whose period contains today, and it's not the same as active (not narrowed away by refinement) - two ideas the names must keep apart. A fact that was wrong (Andrea never owned it) is not ended but retracted: gone from the present and from the past, and still in the log. (planned)
A period on a fact is a fact about a fact, like the condition of a Conditional Relation - the same mechanism should carry both.
Related: R. Snodgrass, "Developing Time-Oriented Database Applications in SQL" (1999); temporal tables in SQL:2011; M. Fowler, "Temporal Patterns" (2005: Temporal Property, Effectivity, Audit Log) and "Bitemporal History" (2021); qualifiers on statements in Wikidata (start time, end time).
Aggregates the actual entities of a type across multiple dimensions: vehicles per status per province, per month. Groups come from relations (status, driver, region) and dates; sums and averages come from the values.
The model already has what a data warehouse builds by hand. A kind is the grain: one row per member. A choice or a list is a dimension. The isA chain of a member is the dimension's hierarchy - InWorkshop is a kind of VehicleStatus, MercedesSprinter a kind of LargeVan a kind of Van - and it's stored precomputed, with distances, so rolling up a level is a lookup. Values are the measures: count, sum, average, lowest, highest.
Two things need care. A member in a list (a vehicle's equipment) counts in several groups, so totals are counted from the rows, never by adding the groups. And with multiple inheritance a member can reach a level by two paths (a trekking bike is both a city bike and a mountain bike): it goes into a group of its own, "City bike + Mountain bike", so every member is counted once and the groups add up - nothing is chosen for the author.
A report is data as well: its groupings, measures and filters are entities referring to the model's, inherited like everything else. Built: reports over a kind with filters, nested groupings, several measures, charts, and every number opening the records behind it. Planned: dashboards whose reports filter each other.
Fallback to code through extensibility points: when a rule can't be said with facts, a small function attached where it's needed - to validate, transform or calculate.
In order of preference: a fact, a rule, an expression, code. Most rules are facts already: Car requires vin; a field type with a minimum, a pattern or a list of allowed values, inherited and narrowed like everything else (BusSeats isA seats, with a lower maximum). Built: those rules, checked on every write. Next: expressions for calculations (cost per unit = materials + machine + labour), each value able to say how it was computed.
An AI can write the expression; a person approves it; only approved versions run - deterministic, the same answer every time, no AI at run time. Code stays the fallback: a function registered by name, pure, sandboxed.
Integrating with system I/O - files, other databases, other services - using Code Extensibility.
For a database, the structure becomes facts and the rows stay where they are. A raw layer mirrors its tables, columns and keys, regenerated whenever the database is read again; a semantic layer is the model's own - Customer isA Party, Customer sourcedFrom sales.customer, companyName mapsTo its column. Every foreign key is one decision, owned or referenced, proposed with its reason. Records are read live when browsed and shaped like entities, inheriting what their type has. Built for Postgres, read only; next, annotating a record with facts the database doesn't have.
Outward, the contract is the resolved model - inheritance and narrowing applied - never raw facts; mappings to other systems are facts too, and every outside system is an identity with its own permissions.
Integrating with the platform's systems - identity, scheduling, notifications - using Code Extensibility.
Authorization is the first of them, and it's facts: who can see or change what is a relation (viewer, editor, owner), checked like a Zanzibar tuple and inherited like any other fact. Actions are the second: every button and every assistant tool calls one named, authorized action, which can be proposed by one person and approved by another. (planned: scheduling, notifications)