Back to Blog

Ontology Modeling Best Practices: 6 Golden Rules

After participating in over 20 Ontology modeling projects, we have identified a pattern: 80% of modeling problems are not technical problems but design decision problems.

CoomiaPublished on August 18, 202520 min read
Share this articleTwitter / X

Ontology Modeling Best Practices: 6 Golden Rules

Series: S4 Ontology Modeling · Article 14 | Level: Intermediate | Reading Time: 18 min

#TL;DR

  • Naming conventions are the foundation of maintainability — adopt "domain-first, English PascalCase, no abbreviations" principles to make ObjectType and property names self-documenting, so new team members can understand the model without reading documentation.
  • Granularity control determines the model's long-term viability — too coarse leads to property explosion, too fine leads to relation explosion. The golden rule is "one ObjectType corresponds to one independently operable business entity."
  • Relation direction, interface extraction, metric design, and version management are the dividing line between "working" and "well-designed" — these four rules elevate your Ontology from PoC-level to production-grade.

#1. Introduction: Why Best Practices Matter

After participating in over 20 Ontology modeling projects, we have identified a pattern: 80% of modeling problems are not technical problems but design decision problems.

Common pitfalls include:

  • Inconsistent naming, where the same concept has different names across ObjectTypes
  • ObjectType granularity out of control, with one "God object" stuffed with 100 properties
  • RelationType direction chaos, with the same relationship defined in both directions
  • InterfaceType not extracted, leading to massive property duplication
  • Metric definitions scattered in code, impossible to manage or audit centrally
  • Schema changes without version control, causing frequent compatibility issues in production

These problems seem trivial at the start, but as the model grows to 50+ ObjectTypes, they become the core source of technical debt.

Code
Model Scale          Common Problem Timeline
───────────────────────────────────────────
5 ObjectTypes        "Just pick a name, let's get it running"
15 ObjectTypes       "Is this property called status or state?"
30 ObjectTypes       "What's the difference between these two ObjectTypes?"
50 ObjectTypes       "Who changed the schema? Why is the API down?"
100 ObjectTypes      "Refactoring cost is too high, just keep adding"

The following 6 golden rules are summarized precisely to prevent this "slippery slope."

#2. Rule One: Naming Conventions — Let Names Speak for Themselves

#2.1 ObjectType Naming

RuleCorrectIncorrectReason
PascalCaseProductionOrderproduction_orderConsistent with Protobuf/Java class names
Singular nounCustomerCustomersObjectType is a type definition, not a collection
Domain nounsWorkOrderWONo abbreviations allowed
No prefixEquipmentTblEquipmentDon't expose storage details
Be specificMaintenanceRecordRecordAvoid over-generalization
Python
# Good naming
object_types = [
    "Customer",
    "ProductionOrder",
    "QualityInspection",
    "MaintenanceSchedule",
    "WarehouseLocation",
]

# Bad naming
bad_names = [
    "Cust",                # Abbreviation
    "tbl_customer",        # Exposes storage
    "CustomerInfoData",    # Redundant suffix
    "Misc",                # Too generic
    "customer",            # Lowercase start
]

#2.2 Property Naming

RuleCorrectIncorrectReason
camelCasecreatedAtcreated_atJSON/TypeScript convention
Meaningful prefixexpectedDeliveryDatedate1Self-documenting
Boolean uses is/hasisActiveactiveType-obvious
Avoid ambiguityorderTotalAmounttotalPrevent cross-type conflicts
Enum uses nounstatusgetStatusProperties are not methods
Python
# Property naming reference table
property_naming = {
    # Time properties
    "createdAt": "TIMESTAMP",      # Creation time
    "updatedAt": "TIMESTAMP",      # Update time
    "scheduledDate": "DATE",       # Scheduled date
    "completedAt": "TIMESTAMP",    # Completion time

    # Boolean properties
    "isActive": "BOOLEAN",         # Whether active
    "hasWarranty": "BOOLEAN",      # Has warranty
    "isOverdue": "BOOLEAN",        # Whether overdue

    # Monetary properties
    "unitPrice": "DOUBLE",         # Unit price
    "totalAmount": "DOUBLE",       # Total amount
    "discountRate": "DOUBLE",      # Discount rate

    # Identifier properties
    "equipmentId": "STRING",       # Equipment ID
    "customerId": "STRING",        # Customer ID
    "externalRef": "STRING",       # External reference
}

#2.3 RelationType Naming

Relation naming follows the "Verb + Noun" pattern, directed from source to target:

Code
Pattern: {Source} --[{Verb}{Target}]--> {Target}

Customer  --[PlacedOrder]-->      Order
Order     --[ContainsProduct]-->  Product
Equipment --[BelongsToLine]-->    ProductionLine
Employee  --[ManagedBy]-->        Employee (self-reference)
WorkOrder --[AssignedTo]-->       Equipment
Python
# Relation naming conventions
relation_examples = [
    # Ownership relations
    RelationType("BelongsToLine",
                 source="Equipment", target="ProductionLine"),
    RelationType("BelongsToDepartment",
                 source="Employee", target="Department"),

    # Action relations
    RelationType("PlacedOrder",
                 source="Customer", target="Order"),
    RelationType("CreatedInspection",
                 source="Inspector", target="QualityInspection"),

    # Containment relations
    RelationType("ContainsItem",
                 source="Order", target="OrderItem"),
    RelationType("HasComponent",
                 source="Equipment", target="Component"),

    # Reference relations
    RelationType("AssignedTo",
                 source="WorkOrder", target="Equipment"),
    RelationType("ReferencesSupplier",
                 source="PurchaseOrder", target="Supplier"),
]

#2.4 Naming Self-Check List

Before creating each ObjectType, use this checklist:

Code
[] Name uses PascalCase?
[] Name is singular noun form?
[] Name is specific enough (not Data, Info, Item, etc.)?
[] Name avoids abbreviations?
[] Name is unambiguous with existing ObjectTypes?
[] Properties use camelCase?
[] Boolean properties start with is/has?
[] Time properties end with At/Date?
[] Relation name expresses semantic direction?

#3. Rule Two: Granularity Control — One ObjectType, One Responsibility

#3.1 Signs of Too-Coarse Granularity

Code
┌────────────────────────────────────────────────────┐
│              Product (too coarse ObjectType)         │
│                                                    │
│  Basic info: name, sku, description, category      │
│  Pricing: basePrice, discountPrice, vipPrice       │
│  Inventory: totalStock, availableStock, reserved   │
│  Supplier: supplierName, supplierContact           │
│  Shipping: weight, dimensions, shippingClass       │
│  Reviews: avgRating, totalReviews, recentComments  │
│  Marketing: tags, promotionId, bannerUrl           │
│                                                    │
│  Property count: 30+                               │
│  Problem: Changing pricing strategy requires       │
│           modifying Product; changing inventory     │
│           logic also requires modifying Product     │
│           -> Violates Single Responsibility         │
└────────────────────────────────────────────────────┘

#3.2 Signs of Too-Fine Granularity

Code
┌───────────┐   ┌───────────┐   ┌───────────┐
│ProductName│   │ProductSku │   │ProductDesc│
│           │   │           │   │           │
│ name      │   │ sku       │   │ desc      │
└─────┬─────┘   └─────┬─────┘   └─────┬─────┘
      │               │               │
      └───────────────┼───────────────┘
                      │
                   Relations: 3+ (just for basic product info)
                   Problem: Querying one product requires
                            JOINing 3 objects -> Over-splitting

#3.3 Correct Granularity Criteria

Golden Rule: One ObjectType corresponds to one independently operable business entity.

Decision criteria:

Question"Yes" -> Independent ObjectType"No" -> Property or StructType
Does it have an independent lifecycle?Order exists independent of CustomerAddress doesn't exist independent of Customer
Does it have an independent primary key?Each equipment has a unique IDEquipment parameters have no independent ID
Can business operate on it independently?Can query/modify inventory aloneWon't query product weight alone
Is it referenced by multiple other objects?Supplier referenced by multiple POsOrder note belongs to only one order
Is its data volume worth independent management?Sensor readings in millionsProduct tags are just a few
Python
# Correct granularity split example
correct_modeling = {
    "Product": {
        "properties": ["productId", "name", "sku", "description",
                       "category", "weight", "dimensions"],
        "structs": ["Dimensions"],  # Nested value object
    },
    "Pricing": {
        "properties": ["pricingId", "basePrice", "currency",
                       "discountRules", "effectiveFrom", "effectiveTo"],
        "reason": "Pricing has independent effective periods and change approval workflows",
    },
    "Inventory": {
        "properties": ["inventoryId", "totalStock", "availableStock",
                       "reservedStock", "warehouseId", "reorderPoint"],
        "reason": "Inventory has independent in/out operations and alert rules",
    },
    "Supplier": {
        "properties": ["supplierId", "name", "contact",
                       "rating", "certifications"],
        "reason": "Supplier is referenced by multiple products/purchase orders",
    },
}

#3.4 When to Use StructType

When a group of properties always appear together but don't need independent management, use StructType:

YAML
# Address — always a component of other objects
kind: StructType
metadata:
  name: Address
spec:
  properties:
    province:
      type: STRING
    city:
      type: STRING
    district:
      type: STRING
    street:
      type: STRING
    postalCode:
      type: STRING
    coordinates:
      type: STRUCT
      structType: GeoLocation

# Reusable across multiple ObjectTypes
# Customer.shippingAddress: Address
# Supplier.headquarterAddress: Address
# Warehouse.location: Address

#4. Rule Three: Relation Direction — From "Who Owns Whom" to "Who Depends on Whom"

#4.1 Direction Selection Principles

Relation direction is not arbitrary — it determines query naturalness and performance:

Code
Principle 1: From "many" to "one" (BelongsTo direction)
─────────────────────────────────────────
Order --[PlacedBy]--> Customer      Good: natural, order belongs to customer
Customer --[HasOrder]--> Order      OK but not preferred

Reason:
- One customer may have 10,000 orders
- Traversing from Order to Customer is 1:1 (fast)
- Traversing from Customer to Orders is 1:N (needs index)
Code
Principle 2: From "dependent" to "dependency"
─────────────────────────────────────
WorkOrder --[AssignedTo]--> Equipment  Good: work order depends on equipment
Equipment --[HasWorkOrder]--> WorkOrder  Not preferred

Reason:
- Deleting equipment requires checking for open work orders
- Dependency direction = cascade check direction
Code
Principle 3: From "action initiator" to "action target"
─────────────────────────────────────
Inspector --[PerformedInspection]--> QualityReport  Good
QualityReport --[InspectedBy]--> Inspector          Not preferred

Reason:
- Action initiator is subject, target is object
- This direction aligns with ActionType semantics

#4.2 Avoid Bidirectional Redundancy

Code
Anti-pattern:
Customer --[HasOrder]-->   Order
Order    --[BelongsTo]--> Customer

Problems:
- Two relations express the same semantics
- Maintenance cost doubles
- Risk of data inconsistency

Correct approach:
Order --[PlacedBy]--> Customer
(Platform automatically supports reverse traversal without declaring reverse relation)
Python
# Platform-supported reverse queries
# Only need to declare one relation
relation = RelationType(
    name="PlacedBy",
    source="Order",
    target="Customer",
    cardinality="MANY_TO_ONE",
)

# Forward query: from order find customer
customer = client.ontology.traverse("Order", order_id, "PlacedBy")

# Reverse query: from customer find all orders (auto-supported)
orders = client.ontology.traverse_reverse("Customer", customer_id, "PlacedBy")

#4.3 Self-Referencing Relations

YAML
# Employee reporting relationship
kind: RelationType
metadata:
  name: ReportsTo
spec:
  source: Employee
  target: Employee
  cardinality: MANY_TO_ONE
  properties:
    since:
      type: DATE
    reportType:
      type: STRING
      enum: [DIRECT, DOTTED_LINE]

# Supported queries:
# - Find someone's direct manager: traverse("Employee", id, "ReportsTo")
# - Find all reports: traverse_reverse("Employee", id, "ReportsTo")
# - Find entire reporting chain: traverse_recursive("Employee", id, "ReportsTo", maxDepth=10)

#4.4 Relation Direction Decision Tree

Code
Q1: Is it a 1:N relationship?
  |-- Yes -> From N side to 1 side (BelongsTo direction)
  |-- No  -> Q2

Q2: Is it a M:N relationship?
  |-- Yes -> Choose "action initiator" as source
  |          (e.g., Student --[Enrolled]--> Course)
  |-- No  -> Q3

Q3: Is it a 1:1 relationship?
  |-- Yes -> From "dependent" to "dependency"
  |          (e.g., UserProfile --[BelongsTo]--> User)
  |-- No  -> May not need RelationType, consider embedded property

#5. Rule Four: Interface Extraction — The Ontology Version of DRY

#5.1 Identifying Repeated Patterns

When you find multiple ObjectTypes sharing the same property combinations, it's time to extract an InterfaceType:

Code
Before (duplicate properties scattered everywhere):

┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│  Equipment    │  │  Vehicle     │  │  Building    │
│              │  │              │  │              │
│  createdAt   │  │  createdAt   │  │  createdAt   │
│  createdBy   │  │  createdBy   │  │  createdBy   │
│  updatedAt   │  │  updatedAt   │  │  updatedAt   │
│  updatedBy   │  │  updatedBy   │  │  updatedBy   │
│  status      │  │  status      │  │  status      │
│  --unique--  │  │  --unique--  │  │  --unique--  │
│  model       │  │  plateNumber │  │  floors      │
│  serialNo    │  │  mileage     │  │  area        │
└──────────────┘  └──────────────┘  └──────────────┘

After (extracted interfaces):

┌──────────────────┐     ┌──────────────────┐
│  <<interface>>    │     │  <<interface>>    │
│  Auditable        │     │  Statusable       │
│                  │     │                  │
│  createdAt       │     │  status           │
│  createdBy       │     │                  │
│  updatedAt       │     └──────────────────┘
│  updatedBy       │
└──────────────────┘

Equipment  implements Auditable, Statusable
Vehicle    implements Auditable, Statusable
Building   implements Auditable, Statusable

#5.2 Common Reusable Interfaces

YAML
# Audit interface — who, when, what
kind: InterfaceType
metadata:
  name: Auditable
spec:
  properties:
    createdAt: { type: TIMESTAMP }
    createdBy: { type: STRING }
    updatedAt: { type: TIMESTAMP }
    updatedBy: { type: STRING }

---
# Geolocation interface — where
kind: InterfaceType
metadata:
  name: Locatable
spec:
  properties:
    latitude: { type: DOUBLE }
    longitude: { type: DOUBLE }
    address: { type: STRING }
    geofenceId: { type: STRING }

---
# State machine interface — current state and transitions
kind: InterfaceType
metadata:
  name: Statusable
spec:
  properties:
    status: { type: STRING }
    statusChangedAt: { type: TIMESTAMP }
    statusChangedBy: { type: STRING }
    previousStatus: { type: STRING }

---
# Tagging interface — classification and labels
kind: InterfaceType
metadata:
  name: Taggable
spec:
  properties:
    tags: { type: ARRAY, itemType: STRING }
    category: { type: STRING }
    labels: { type: MAP, keyType: STRING, valueType: STRING }

---
# Measurable interface — has associated metrics
kind: InterfaceType
metadata:
  name: Measurable
spec:
  properties:
    lastMeasuredAt: { type: TIMESTAMP }
    measurementCount: { type: INTEGER }
    measurementSource: { type: STRING }

---
# Archivable interface — soft delete and archive
kind: InterfaceType
metadata:
  name: Archivable
spec:
  properties:
    isArchived: { type: BOOLEAN }
    archivedAt: { type: TIMESTAMP }
    archivedBy: { type: STRING }
    archiveReason: { type: STRING }

#5.3 The Power of Polymorphic Queries

Python
# Find all Locatable objects "in Shanghai" (regardless of Equipment, Vehicle, or Building)
result = client.ontology.query(
    interface="Locatable",
    filter="address LIKE '%Shanghai%'",
)

# Returns mixed results: Equipment + Vehicle + Building
for obj in result.objects:
    print(f"[{obj.object_type}] {obj.name} at {obj.address}")

# Find all Auditable objects "modified in the last 24 hours"
recent_changes = client.ontology.query(
    interface="Auditable",
    filter="updatedAt > now() - interval('24h')",
    order_by="updatedAt DESC",
)

#5.4 Interface Extraction Decision Criteria

Code
Extract an interface when any of these conditions are met:

1. Property combination repeats in 3+ ObjectTypes
2. Cross-type polymorphic query requirement exists
3. Property combination represents an independent business capability
   (e.g., "auditable", "locatable")
4. Need to uniformly apply rules or permissions to heterogeneous objects

#6. Rule Five: Metric Design — The Bridge from "Data" to "Insight"

#6.1 Metric Definition Principles

Code
Principle 1: Every metric must have clear business meaning
─────────────────────────────────────
Good: "Overall Equipment Effectiveness (OEE)" — managers understand immediately
Bad:  "avg_val_123" — nobody knows what this is

Principle 2: Metrics must declare aggregation method
─────────────────────────────
Good: AVG(oee) GROUP BY productionLine — average OEE per line
Bad:  oee — unclear if single value or aggregate

Principle 3: Metrics must declare dimensions
─────────────────────────
Good: dimensions: [factory, productionLine, shift]
Bad:  No dimensions = only global value

Principle 4: Derived metrics must declare dependencies
─────────────────────────────
Good: oee = availability * performance * quality
Bad:  Opaque calculation relationships between metrics

#6.2 Metric Hierarchy Design

Code
Level 4: Strategic Metrics (CEO/CFO)
|-- Enterprise-wide OEE
|-- Customer Satisfaction (NPS)
|-- Revenue Achievement Rate
|
Level 3: Operational Metrics (Department Managers)
|-- Line OEE
|-- Order On-Time Delivery Rate
|-- Inventory Turnover Rate
|
Level 2: Tactical Metrics (Shift Supervisors)
|-- Equipment Availability
|-- First Pass Yield
|-- Work Order Completion Rate
|
Level 1: Atomic Metrics (Sensors/Systems)
|-- Equipment Running Hours
|-- Product Inspection Results
|-- Work Order Status Changes
Python
# Level 1: Atomic metrics (directly from data)
atomic_metrics = [
    MetricSpec(
        name="EquipmentRuntime",
        object_type="Equipment",
        property="runningHours",
        aggregation="SUM",
        unit="hours",
        dimensions=["factory", "productionLine"],
    ),
    MetricSpec(
        name="InspectionPassCount",
        object_type="QualityInspection",
        property="isPassed",
        aggregation="COUNT",
        filter="isPassed == true",
        dimensions=["productionLine", "shift"],
    ),
]

# Level 2: Tactical metrics (computed from atomic metrics)
tactical_metrics = [
    MetricSpec(
        name="EquipmentAvailability",
        expression="EquipmentRuntime / PlannedProductionTime",
        unit="percentage",
        dimensions=["factory", "productionLine"],
    ),
    MetricSpec(
        name="FirstPassYield",
        expression="InspectionPassCount / TotalInspectionCount",
        unit="percentage",
        dimensions=["productionLine", "shift"],
    ),
]

# Level 3: Operational metrics (computed from tactical metrics)
operational_metrics = [
    MetricSpec(
        name="LineOEE",
        expression="EquipmentAvailability * PerformanceRate * QualityRate",
        unit="percentage",
        dimensions=["factory", "productionLine"],
    ),
]

# Level 4: Strategic metrics (aggregated from operational metrics)
strategic_metrics = [
    MetricSpec(
        name="EnterpriseOEE",
        expression="AVG(LineOEE)",
        unit="percentage",
        dimensions=["factory"],
    ),
]

#6.3 Metric Alert Thresholds

Python
# Configure alerts for metrics
client.metric.set_alert(
    metric="LineOEE",
    rules=[
        AlertRule(
            name="oee-critical",
            condition="value < 0.60",
            severity="CRITICAL",
            message="Line {productionLine} OEE below 60%",
            channels=["sms-factory-manager", "pagerduty"],
        ),
        AlertRule(
            name="oee-warning",
            condition="value < 0.75",
            severity="WARNING",
            message="Line {productionLine} OEE below 75%",
            channels=["slack-production"],
        ),
        AlertRule(
            name="oee-trend-down",
            condition="trend(7d) < -0.05",
            severity="INFO",
            message="Line {productionLine} OEE 7-day trend down 5%",
            channels=["email-production-manager"],
        ),
    ],
)

#6.4 Metric Version Management

Python
# Metric definition changes require approval
proposal = client.metric.create_change_proposal(
    metric="LineOEE",
    change_type="FORMULA_UPDATE",
    old_expression="availability * performance * quality",
    new_expression="availability * performance * quality * sustainability",
    reason="Add sustainability factor for ESG reporting requirements",
    effective_date="2026-04-01",
)

# Submit for review
proposal.submit_for_review(reviewers=["data-governance-team"])

#7. Rule Six: Version Management — The Safety Net for Schema Changes

#7.1 Why Schema Version Management is Critical

Code
World without version management:

Day 1: Add property email
Day 3: Rename email to emailAddress
Day 5: 10 downstream applications all break
Day 6: Emergency rollback, but data is already inconsistent
Day 7: Overtime fixing data

World with version management:

Day 1: Create Proposal "Add emailAddress property"
Day 2: Automatic compatibility check passes
Day 3: Reviewer approves
Day 4: Canary release to 10% of readers
Day 5: Full rollout
Day 6: Old property email marked DEPRECATED
Day 30: Confirmed no one uses it, remove old property

#7.2 Schema Change Classification

Change TypeCompatibilityAuto-approveExample
Add optional propertyBackward compatibleYesAdd nickname: STRING?
Add required property (with default)Backward compatibleYesAdd status: STRING = "ACTIVE"
Add required property (no default)IncompatibleNoAdd email: STRING
Remove propertyIncompatibleNoRemove fax: STRING
Change property typeIncompatibleNoage: STRING -> INTEGER
Rename propertyIncompatibleNoemail -> emailAddress
Add relationBackward compatibleYesAdd BelongsTo
Remove relationIncompatibleNoRemove BelongsTo

#7.3 Proposal Workflow

Code
┌─────────┐   submit    ┌──────────┐   approve   ┌──────────┐
│  DRAFT   ├────────────►│  REVIEW  ├────────────►│ APPROVED │
└────┬────┘             └────┬─────┘             └────┬─────┘
     │                       │                        │
     │ discard          reject│                   apply│
     │                       │                        │
     ▼                       ▼                        ▼
┌─────────┐           ┌──────────┐            ┌──────────┐
│DISCARDED│           │ REJECTED │            │ APPLIED  │
└─────────┘           └──────────┘            └──────────┘
Python
# Create schema change proposal
proposal = client.schema.create_proposal(
    title="Add loyalty-related properties to Customer",
    description="Support loyalty points system launch",
    changes=[
        AddProperty("Customer", "loyaltyTier",
                    type="STRING", enum=["BRONZE","SILVER","GOLD","PLATINUM"],
                    default="BRONZE"),
        AddProperty("Customer", "loyaltyPoints",
                    type="INTEGER", default=0),
        AddProperty("Customer", "memberSince",
                    type="DATE", nullable=True),
        AddRelation("Customer", "EarnedReward", "Reward",
                    cardinality="ONE_TO_MANY"),
    ],
)

# Submit for review
proposal.submit(reviewers=["schema-admin", "loyalty-team-lead"])

# Check compatibility results
compat = proposal.compatibility_check()
print(f"Backward compatible: {compat.backward_compatible}")
print(f"Forward compatible: {compat.forward_compatible}")
print(f"Breaking changes: {compat.breaking_changes}")

# Apply after approval
proposal.apply(
    rollout_strategy="CANARY",      # Canary release
    canary_percentage=10,            # 10% canary first
    auto_promote_after="24h",        # Auto-promote after 24h if no issues
)

#7.4 Version Number Strategy

Code
Schema versioning follows semantic versioning: MAJOR.MINOR.PATCH

MAJOR: Incompatible changes
  - Remove property
  - Change property type
  - Remove relation

MINOR: Backward-compatible changes
  - Add optional property
  - Add relation
  - Add interface implementation

PATCH: Non-structural changes
  - Modify display_name
  - Modify description
  - Modify constraints (relaxation)

Example version timeline:
Customer v1.0.0 -> v1.1.0 (add loyaltyTier) -> v1.1.1 (update description)
         -> v2.0.0 (remove fax property, change phone type)

#8. Comprehensive Practice: Refactoring an Ontology with All 6 Rules

#8.1 Before Refactoring (Violating Multiple Rules)

Python
# Anti-pattern example
bad_ontology = {
    "cust_info": {  # Violates naming: abbreviation, underscore, "info" suffix
        "properties": {
            "id": "STRING",
            "nm": "STRING",            # Abbreviation
            "created": "STRING",       # Wrong type, should be TIMESTAMP
            "addr_province": "STRING", # Should use StructType
            "addr_city": "STRING",
            "addr_street": "STRING",
            "total_orders": "INTEGER", # Where is the computation logic?
            "vip": "BOOLEAN",          # Should be isVip
        },
    },
    "order_data": {  # "data" suffix is meaningless
        "properties": {
            "oid": "STRING",
            "cust_id": "STRING",       # Should be RelationType
            "items": "STRING",         # JSON string? Should be relation
        },
    },
}

#8.2 After Refactoring (Following All 6 Rules)

Python
# Best practice example

# Rule 1: Naming conventions
# Rule 4: Interface extraction
interfaces = [
    InterfaceType("Auditable", properties=[
        "createdAt", "createdBy", "updatedAt", "updatedBy",
    ]),
    InterfaceType("Archivable", properties=[
        "isArchived", "archivedAt", "archivedBy",
    ]),
]

# Rule 2: Granularity control (Address as StructType)
structs = [
    StructType("Address", properties={
        "province": "STRING",
        "city": "STRING",
        "district": "STRING",
        "street": "STRING",
        "postalCode": "STRING",
    }),
]

# Rule 2: Granularity control (independent business entities)
object_types = [
    ObjectType("Customer",
        implements=["Auditable", "Archivable"],
        properties={
            "customerId": PropertySpec(type="STRING", primary_key=True),
            "name": PropertySpec(type="STRING", required=True),
            "email": PropertySpec(type="STRING"),
            "shippingAddress": PropertySpec(type="STRUCT",
                                           struct_type="Address"),
            "isVip": PropertySpec(type="BOOLEAN", default=False),
            # Rule 5: Metric design - derived property declares dependency
            "totalOrders": PropertySpec(type="INTEGER", derived=True,
                                       aggregation="COUNT",
                                       source_relation="PlacedOrder"),
        },
    ),
    ObjectType("Order",
        implements=["Auditable"],
        properties={
            "orderId": PropertySpec(type="STRING", primary_key=True),
            "totalAmount": PropertySpec(type="DOUBLE"),
            "status": PropertySpec(type="STRING",
                                   enum=["PENDING","PAID","SHIPPED","COMPLETED"]),
        },
    ),
    ObjectType("OrderItem",
        properties={
            "itemId": PropertySpec(type="STRING", primary_key=True),
            "quantity": PropertySpec(type="INTEGER"),
            "unitPrice": PropertySpec(type="DOUBLE"),
        },
    ),
]

# Rule 3: Relation direction (from N-side to 1-side)
relations = [
    RelationType("PlacedBy", source="Order", target="Customer",
                 cardinality="MANY_TO_ONE"),
    RelationType("ContainsItem", source="Order", target="OrderItem",
                 cardinality="ONE_TO_MANY"),
]

# Rule 6: Version management
proposal = schema.create_proposal(
    title="Customer Loyalty System v1.1.0",
    changes=[...],
)

#9. Common Anti-Pattern Checklist

Anti-PatternDescriptionFix
God ObjectOne ObjectType with 50+ propertiesSplit by responsibility into multiple ObjectTypes
Anemic ObjectObjectType has only properties, no ActionTypesBind business operations
Spaghetti RelationsEvery pair of ObjectTypes has a relationReview necessity, remove redundant relations
Interface AvoidanceRepeated properties scattered everywhereExtract commonalities into InterfaceType
Metric SprawlMetrics grow without hierarchy or governanceEstablish 4-level metric hierarchy
Schema CowboyDirectly modifying production schemaEnforce Proposal workflow
Name InconsistencySame concept with multiple namesEstablish and maintain naming dictionary
Over-NormalizationOver-splitting causes N+1 queriesMerge frequently co-queried properties

#10. Modeling Review Checklist

Use this checklist in every Ontology modeling review:

Code
Naming Convention (Rule 1)
[] All ObjectTypes use PascalCase
[] All properties use camelCase
[] No abbreviations, no prefixes, no redundant suffixes
[] Boolean properties start with is/has
[] Relation names express semantic direction

Granularity Control (Rule 2)
[] Each ObjectType maps to one independent business entity
[] No ObjectType has more than 25 properties
[] Nested value objects use StructType
[] No ObjectType with only 1-2 properties

Relation Direction (Rule 3)
[] 1:N relations go from N side to 1 side
[] No bidirectional redundant relations
[] Relation names follow "Verb+Noun" format
[] Self-referencing relations have clear semantics

Interface Extraction (Rule 4)
[] Property combinations appearing 3+ times extracted as interfaces
[] Audit properties unified into Auditable interface
[] Geo-location properties unified into Locatable interface
[] Interface names are adjectives (able/ible suffix)

Metric Design (Rule 5)
[] Metrics have clear business meaning and units
[] Metrics declare aggregation method and dimensions
[] Derived metrics declare dependencies
[] Key metrics have alert thresholds configured

Version Management (Rule 6)
[] All schema changes go through Proposal workflow
[] Incompatible changes have migration plans
[] Version numbers follow semantic versioning
[] Deprecated properties have clear removal schedules

#Key Takeaways

  1. Naming conventions are not "minor details." In Ontology modeling, names are the API, the documentation, and the shared language of team communication. PascalCase ObjectTypes, camelCase properties, semantic relation names — these three rules reduce 50% of communication overhead.

  2. Granularity is both "art" and "science." Use "independently operable business entity" as the judgment criterion and StructType for nested value objects, avoiding both God Objects and over-splitting.

  3. Relation direction determines query efficiency. From "many" to "one," from "dependent" to "dependency" — these two principles multiply your Ontology graph query performance.

  4. Interface extraction is the Ontology version of DRY. Universal interfaces like Auditable, Locatable, and Taggable not only reduce duplication but also enable powerful polymorphic query capabilities.

  5. Metrics must be governed in layers. The 4-level hierarchy from atomic to strategic metrics ensures each role sees the data insights they care about.

  6. Schema changes must go through the Proposal workflow. Compatibility checks, approval, canary release — this workflow is the last line of defense for production stability.

#Next Article

S4-15: Ontology Practice: E-Commerce Modeling — We will apply all 6 golden rules to a complete e-commerce scenario (Product/Order/User/Inventory/Logistics).

tags: ontology, best-practices, naming-convention, granularity, relation-direction, interface-extraction, metric-design, schema-versioning, coomia-dip