Ontology engineering · anti-patterns

How a model goes wrong.

A field guide to the anti-patterns that recur in ontology modeling — the category errors, the misread logic, and the discipline lapses — each with the tell, the damage, and the fix.

Written for Modelers Reviewers who want to smell a mistake early

Why a catalog

Most modeling mistakes are not exotic. The same handful recur across every domain, and once you can name them you can see them in a review before they harden into axioms that thousands of queries depend on. This is that catalog — the traps that recur in insurance ontologies and everywhere else — written so you can spot the tell, understand the damage, and apply the fix.

The thesis

Nearly every anti-pattern is one of three failures: you modeled the wrong kind of thing, you forgot how the logic actually behaves, or you skipped the discipline. Learn to recognise those three and you can smell the rest.

root one

Category errors

Confusing what kind of thing you are modeling — an entity with its attribute, a class with a value, a rigid type with a role.

root two

Fighting the logic

Forgetting that OWL is open-world, monotonic, and makes no unique-name assumption — so it infers where you expected it to reject.

root three

Skipping the discipline

No competency questions, no modules, no naming policy, no versioning — the model grows without a shape or a stop condition.

The families below are ordered by those roots. Several anti-patterns have a full brief of their own in this series; those are collected as a hub in §05 rather than repeated here.

01 — CATEGORY ERRORS

Modeling the wrong kind of thing

A category error puts a thing in the wrong bucket entirely — and because the reasoner will faithfully draw conclusions from it, the mistake propagates instead of surfacing. These are the most damaging and the easiest to make.

AP-1 is-a vs has-a

Making an entity a subclass of its own attribute

A policy runs for a period, so it is tempting to make it a kind of time interval. But a policy is an information entity that has a term; it is not itself an interval. The same slip turns a claim into a money amount, or a party into an address.

The tell: a subclass axiom where the parent is a property of the child — time, money, location, a code — not a genuine super-type.

policy:Policy rdfs:subClassOf time:DateTimeInterval .   # a policy IS an interval? no.

# The fix: the policy HAS a term; the term is the interval.
policy:Policy   rdfs:subClassOf core:InformationEntity .
agr:hasTerm     a owl:FunctionalProperty ;
                rdfs:domain agr:Agreement ; rdfs:range agr:AgreementTerm .
agr:AgreementTerm rdfs:subClassOf time:DateTimeInterval .   # the *term* is the interval
AP-2 is-a vs part-of

Using subclass for composition

A coverage belongs to a policy, so it gets filed under it with rdfs:subClassOf. But subsumption means “every coverage is a policy,” which is false. The relationship is mereological — a coverage is a part of a policy.

The test: read the axiom aloud as “every X is a Y.” If that is false but “X is a component of Y” is true, you want a part-of property, not a subclass.

coverage:Coverage rdfs:subClassOf policy:Policy .   # a coverage is not a kind of policy

# The fix: relate the whole to its part.
policy:hasCoverage a owl:ObjectProperty ;
        rdfs:domain policy:Policy ; rdfs:range coverage:Coverage ;
        rdfs:subPropertyOf core:hasPart .
AP-3 class vs value

Modeling a controlled value as a class

Perils, claim statuses, currencies and lines of business feel like classes, so each code becomes an owl:Class. A few hundred trivial subclasses later, the reasoner is classifying a code list and the business cannot add a value without a modeling change. These are values, not types.

The tell: a broad, flat subclass tree whose leaves are never instantiated — they only ever label something.

peril:Flood rdfs:subClassOf peril:Peril .   # hundreds of value-classes in the T-Box

# The fix: a SKOS concept in a scheme — governed data, referenced by the model.
peril:Flood a skos:Concept ; skos:inScheme peril: ;
        skos:prefLabel "Flood"@en ; skos:notation "FLD" .

Full brief: A vocabulary you can govern — how code lists, taxonomies, thesauri and glossaries belong in SKOS.

AP-4 rigid type vs role

Making a role a rigid subclass of the player

An underwriter is a person, so Underwriter ⊑ Person. But underwriter is a role a party plays, not a rigid kind it always is: the same person may also be a claimant, and may stop underwriting tomorrow. Baking the role into the class makes membership permanent and multiplies a person across every hat they wear. (This is the anti-rigidity pitfall from OntoClean.)

The tell: a subclass of Person or Organization named after a job, a relationship or a temporary state.

policy:Underwriter rdfs:subClassOf core:Person .   # once a person, forever an underwriter?

# The fix: reify the role; the person fills it via hasRole.
policy:UnderwriterRole a agr:PartyRole ; core:roleProperty policy:underwriter .
policy:Underwriter owl:equivalentClass
        [ owl:onProperty core:hasRole ; owl:hasValue policy:UnderwriterRole ] .
02 — FIGHTING THE LOGIC

Forgetting how OWL actually behaves

OWL is not a database and not a schema validator. It is an open-world, monotonic logic with no unique-name assumption. Half of all “the reasoner is broken” reports are really a modeler expecting closed-world, validating behaviour from a system built to infer, never to reject.

AP-5 open world

Using OWL as a validator

You want “every policy must have a policy number, or reject it,” so you write a minCardinality 1 restriction — and nothing gets rejected. Under the open-world assumption, a missing value is unknown, not absent: OWL infers the number exists somewhere, it does not flag its absence.

What OWL does

  • Infers. “There exists a policy number” — even if none is stated.
  • Never rejects. Missing data is unknown, not invalid.
  • A cardinality axiom is a meaning, not a check.

What SHACL does

  • Closes the world. sh:minCount 1 means “present, or fail.”
  • Reports violations against real data.
  • Model meaning in OWL; enforce quality in SHACL.
AP-6 no unique names

Assuming two different IRIs are two different things

OWL makes no unique-name assumption: ex:PartyA and ex:PartyB are not known to be distinct unless you say so. This surprises people through functional properties — asserting two values does not raise an error; it makes the two values owl:sameAs each other.

The tell: a functional property that “should have caught” a double-assignment but silently merged two individuals instead.

# hasPrimaryInsured is functional; two claimants are asserted…
ex:Claim-1 claim:hasClaimant ex:PartyA , ex:PartyB .
# …OWL does NOT complain — it infers ex:PartyA owl:sameAs ex:PartyB .

# The fix: assert distinctness (or class disjointness) so the clash is real.
[] a owl:AllDifferent ; owl:distinctMembers ( ex:PartyA ex:PartyB ) .
AP-7 empty axioms

Writing restrictions that say nothing

A restriction can be syntactically valid and logically vacuous. minCardinality 0 is always true. someValuesFrom owl:Thing only says “has at least one of something.” They look like constraints and constrain nothing — noise a reviewer must decode and a reasoner must carry.

[ a owl:Restriction ; owl:onProperty policy:hasCoverage ; owl:minCardinality 0 ]   # always true
[ a owl:Restriction ; owl:onProperty policy:hasCoverage ; owl:someValuesFrom owl:Thing ] # ≥1 anything

# The fix: state the constraint you actually mean, or delete it.
[ a owl:Restriction ; owl:onProperty policy:hasCoverage ; owl:someValuesFrom coverage:Coverage ]
AP-8 some vs all

Reading allValuesFrom as “must have one”

allValuesFrom (∀) constrains only the values that are present; it does not require any to exist. A claim with zero losses satisfies “all losses are of type Loss” perfectly. If you meant “a claim has a loss,” you needed someValuesFrom (∃) or a qualified minimum.

claim:Claim rdfs:subClassOf [ owl:onProperty claim:hasLoss ; owl:allValuesFrom claim:Loss ] .
# satisfied by a claim with NO loss at all — probably not what you meant

# The fix: require existence.
claim:Claim rdfs:subClassOf [ owl:onProperty claim:hasLoss ; owl:someValuesFrom claim:Loss ] .
AP-9 DL profile

Putting cardinality on a non-simple property

Cardinality, functional and several other axioms are only legal in OWL 2 DL on simple properties — ones with no transitive sub-property. Hang a count on core:hasPart (transitive) and the ontology silently leaves the DL profile, so the reasoner you validate with may refuse it or fall back to a weaker logic.

The tell: a cardinality or FunctionalProperty axiom on a property that is, or has, a transitive sub-property.

core:hasPart a owl:TransitiveProperty , owl:FunctionalProperty .  # illegal in OWL 2 DL

# The fix: keep counts on a simple property designed for it.
agr:hasTerm a owl:FunctionalProperty ; rdfs:range agr:AgreementTerm .  # simple by design
03 — PROPERTIES & AXIOMS

Axioms that do more, or less, than you think

AP-10 domain & range

Treating rdfs:domain / rdfs:range as constraints

Domain and range are not checks — they are inference rules. Declaring rdfs:domain policy:Policy does not forbid using the property elsewhere; it concludes that whatever you used it on is a policy. Point it at a claim, and if Policy and Claim are disjoint you have not caught an error — you have made the ontology inconsistent.

What you expected

  • “This property is only allowed on a Policy.”
  • Using it elsewhere is rejected.

What actually happens

  • Anything with this property is inferred to be a Policy.
  • Over-broad domains silently reclassify individuals — or blow up as unsatisfiable.
  • For “only allowed on,” use a SHACL sh:targetClass shape.
AP-11 missing disjointness

Sibling classes with no disjointness

Without owl:disjointWith, nothing stops an individual from being both a Person and an Organization, or a claim event from being an opening and a closing. Because OWL never assumes classes are separate, the contradiction you are counting on to catch a bug is never derived. Disjointness is what gives a reasoner something to falsify.

# Without this, Person ⊓ Organization is perfectly consistent — and bugs hide.
[] a owl:AllDisjointClasses ;
   owl:members ( core:Person core:Organization ) .

Balance it: disjointness is powerful but committal. Declare partitions you are sure of; over-eager disjointness between classes that legitimately overlap is its own anti-pattern, producing unsatisfiable classes downstream.

04 — IDENTITY & NAMES

Where a name betrays you

AP-12 phantom terms

A typo mints a brand-new term

Write core:partipatesIn instead of core:participatesIn and nothing complains. In an open world, an unrecognised IRI is not a mistake — it is simply a new property with no domain, range or inverse. The triple is valid; no inference fires; the real property’s axioms never touch your data. The error is invisible to the reasoner and shows up only as a competency question that quietly returns nothing.

The tell: a query that should return rows returns none, and the culprit term appears exactly once in the whole graph.

ex:Claim-1 core:partipatesIn ex:PartyA .   # typo — a new, meaningless property
# the intended core:participatesIn, with its inverse and axioms, never applies

The fix: the reasoner cannot help here — add a closed-world guard. A SHACL shape or a lint that every predicate resolves to a term declared in an imported ontology catches phantom IRIs; consistent prefixes and CQ tests catch the rest.

05 — DISCIPLINE LAPSES

Anti-patterns with a brief of their own

The third root — skipping the discipline — produces anti-patterns big enough to each warrant a full brief. They are catalogued here and treated in depth elsewhere in this series.

06 — QUICK REFERENCE

The whole catalog, at a glance

Twenty anti-patterns, the tell that gives each one away, and the move that fixes it. Keep it beside a review.

#Anti-patternThe tellThe fix
AP-1Entity as subclass of its attributeparent is a property (time, money) of the childhas-a via a property; keep the entity its own kind
AP-2Subclass for composition“every X is a Y” reads falsea part-of property under core:hasPart
AP-3Value modeled as a classflat subclass tree, leaves never instantiatedSKOS concept in a scheme · brief
AP-4Role as a rigid subclasssubclass of Person/Org named after a jobreify the role; fill it with core:hasRole
AP-5OWL used as a validatorcardinality that never rejects anythingSHACL for closed-world checks
AP-6Assuming unique namesfunctional property merged two individualsowl:AllDifferent / disjointness
AP-7Vacuous restrictionsminCardinality 0, someValuesFrom Thingstate a real constraint, or delete it
AP-8allValuesFrom read as “must have”restriction satisfied by zero valuessomeValuesFrom or qualified min
AP-9Cardinality on a non-simple propertycount on a transitive propertyput counts on a simple property
AP-10Domain/range as constraintsproperty reused “where it shouldn’t be”expect inference; validate in SHACL
AP-11Missing disjointnesscontradictions never surfaceAllDisjointClasses for real partitions
AP-12Typo mints a phantom terma CQ returns nothing; term used onceclosed-world lint that predicates are declared
AP-13Opaque / version-bearing IRIsversion in the namespace, labels as IRIsstable slash IRIs, version in versionIRI · brief
AP-14Re-minting standard vocabulariesSKOS/Org/Time copied under your hostreuse at canonical IRIs · brief
AP-15Modeling the message, not the domainclasses shaped like a JSON/XML payloadmine terms; derive structure from the domain · brief
AP-16The God ontologyone enormous file, everything coupledcohesive, layered modules · brief
AP-17Import cyclesmodules import each othera directed, acyclic import graph · brief
AP-18No competency questionsno test of what the model must answerquestions drive scope and prove doneness · brief
AP-19No versioning / deprecationterms deleted in place; namespace mutatedversion with intent; deprecate over delete · brief
AP-20Premature abstractionmetaphysics no question needsmodel to the questions; abstract on the second case

✓ How to use this

In a review, walk the diff against the three roots: is every new subclass a genuine is-a (not AP-1 to AP-4)? Does every restriction mean what its author thinks under open-world, no-UNA semantics (AP-5 to AP-9)? And do the axioms and names do exactly what they claim (AP-10 to AP-14)? Most bad axioms fail one of those three questions.