Back to Blog

Ontology as API: Why Ontology Models Beat REST APIs as Contracts

Every engineer who has built a large-scale platform has lived through this nightmare:

CoomiaPublished on December 15, 20259 min read
Share this articleTwitter / X

Ontology as API: Why Ontology Models Beat REST APIs as Contracts

Series: S10 Design Patterns · Article 1 | Level: Advanced | Reading Time: 18 min

#TL;DR

  • Traditional REST APIs center on "endpoints." Every new business scenario requires a new set of endpoints, leading to endpoint explosion and version hell.
  • The Ontology-as-API pattern uses ontology models as the contract layer. All operations target "object-relation-property" triples rather than hardcoded endpoint paths.
  • coomia-dip auto-generates gRPC services, SDK clients, and permission policies from Ontology Schema — model once, use everywhere.

#Introduction: The API Design Dilemma

Every engineer who has built a large-scale platform has lived through this nightmare:

Code
/api/v1/users
/api/v1/users/{id}/orders
/api/v1/users/{id}/orders/{orderId}/items
/api/v2/users/{id}/orders/{orderId}/items  # v2 added discount field
/api/v1/users/{id}/recommendations         # new requirement
/api/v1/users/{id}/risk-score              # yet another requirement

Every new business requirement means a new endpoint. Every field change means a new version. You think you are doing API design, but you are actually doing endpoint operations.

And this is just one microservice. When you have 20 microservices, each with 30 endpoints, and 3 concurrent versions, you are staring at 1,800 endpoints. No documentation can keep up, and no team can guarantee their consistency.

The root cause is not that REST is bad — it is that endpoint-centric contract models are structurally unfit for complex business semantics.

#Part 1: Three Structural Flaws of REST APIs

#1.1 Coupling Between Endpoints and Business Logic

REST's design philosophy is "resource + verb." This works beautifully for simple CRUD:

Code
GET    /users       -> List users
POST   /users       -> Create user
GET    /users/{id}  -> Get user
PUT    /users/{id}  -> Update user
DELETE /users/{id}  -> Delete user

But real business is not CRUD. When you need to "transfer a user from Department A to Department B, simultaneously updating permissions, notifying their manager, and recording an audit log," which endpoint do you call?

Code
PUT /users/{id}/department?to=B           # Option 1
POST /users/{id}/transfer                 # Option 2
POST /operations/user-transfer            # Option 3

All three are compromises. REST's "resource + verb" model simply lacks the expressive power for business operations. You end up inventing new endpoints beyond standard verbs — that is why REST APIs inevitably degenerate into RPC-over-HTTP.

#1.2 Insufficient Expressiveness for Cross-Entity Queries

Business analysts frequently ask: "Find all users who placed orders in the last 30 days with a risk score above 80, group them by department, and show average order value per department."

Answering this with REST requires multiple endpoint calls plus client-side aggregation — the infamous N+1 query problem. GraphQL partially solves it but introduces query parsing, depth limits, and cache invalidation complexity.

#1.3 The Bottomless Pit of Version Management

Adding a field to a response forces every consumer to handle the change. You face three undesirable choices: breaking changes, version explosion, or shifting complexity to callers. With 8 Layers, 63 proto files, and hundreds of services, traditional REST versioning would require a dedicated team.

#Part 2: The Core Idea of Ontology-as-API

#2.1 The Key Insight

API contracts should not be endpoint lists — they should be business object models.

  • Traditional REST: Contract = URL paths + request/response JSON Schema
  • Ontology-as-API: Contract = Object types + property definitions + relationship definitions + action definitions

You no longer design endpoints — you design ontology models:

YAML
ObjectType: TransferOrder
  Properties:
    - sourceWarehouse: Link<Warehouse>
    - targetWarehouse: Link<Warehouse>
    - items: List<Link<InventoryItem>>
    - status: Enum<Draft, Pending, Approved, Executed, Cancelled>
  Actions:
    - approve(approver: User, comment: String) -> TransferOrder
    - execute(executor: User) -> TransferOrder
    - cancel(reason: String) -> TransferOrder

All platform capabilities — query, mutation, subscription, permissions — are automatically generated from this model.

#2.2 Automatic Derivation from Model to API

Once you define an Ontology Schema in coomia-dip, the following are auto-generated:

Code
ObjectType Definition
    |
    v
  gRPC Service       -> Typed CRUD + Action interfaces
  Python SDK          -> client.TransferOrder.get(id)
  TypeScript SDK      -> await client.TransferOrder.get(id)
  Permission Rules    -> ABAC policies based on object types
  Subscription        -> Real-time object change notifications
  Audit Trail         -> Automatic operation logging

The key: generated APIs inherently understand business semantics. When you define an approve Action on TransferOrder, the system knows this requires permission checks, triggers state transitions, and needs audit logging.

#2.3 Comparison with Palantir Foundry

Palantir Foundry pioneered Ontology-as-API. coomia-dip improves on it with:

  1. Open-source transparency: Schema definitions and derivation logic are fully open source
  2. gRPC core: All internal communication uses gRPC for better performance than Foundry's REST core
  3. Multi-language SDK: Auto-generates typed SDKs for Python, TypeScript, and Java

#Part 3: Implementation Architecture in coomia-dip

#3.1 Schema Registry: The Metadata Hub

The Control Layer (Control Layer) contains the Schema Registry, which stores and manages all Ontology Schema definitions. It maintains a complete type relationship graph — which types are related, which properties are derived, which Actions trigger cascading operations.

Code
Schema Registry
  |-- ObjectTypes: User, Department, Order, Product, ...
  |-- RelationTypes: User->Department, User->Order, ...
  |-- Property Schemas
  |-- Action Definitions
      |
      v
  API Gateway    -> Dynamic routing based on Schema
  SDK Generator  -> Generates typed clients from Schema
  Permission     -> Generates ABAC policies from Schema

#3.2 Auto-Generated gRPC Services

When a new ObjectType is registered, the Control Layer auto-generates corresponding gRPC service definitions:

PROTOBUF
service TransferOrderService {
  rpc Get(GetTransferOrderRequest) returns (TransferOrder);
  rpc List(ListTransferOrdersRequest) returns (ListTransferOrdersResponse);
  rpc Create(CreateTransferOrderRequest) returns (TransferOrder);
  rpc Update(UpdateTransferOrderRequest) returns (TransferOrder);
  rpc Delete(DeleteTransferOrderRequest) returns (Empty);
  rpc Approve(ApproveTransferOrderRequest) returns (TransferOrder);
  rpc Execute(ExecuteTransferOrderRequest) returns (TransferOrder);
  rpc Cancel(CancelTransferOrderRequest) returns (TransferOrder);
  rpc Subscribe(SubscribeTransferOrderRequest) returns (stream TransferOrderEvent);
  rpc GetRelated(GetRelatedRequest) returns (GetRelatedResponse);
}

Key: Actions are first-class citizens, subscriptions are built-in, relationship queries are built-in.

#3.3 Type-Safe SDK Clients

Python
from ontology_sdk import OntoPlatform

platform = OntoPlatform(endpoint="grpc://control-Layer:9090")

# Type-safe object operations
order = platform.objects.TransferOrder.get("order-123")
print(order.source_warehouse.name)

# Type-safe Action invocation
approved_order = order.approve(approver=current_user, comment="Approved")

# Type-safe querying
high_value_orders = (
    platform.objects.TransferOrder
    .where(lambda o: o.total_value > 100000)
    .where(lambda o: o.status == "Pending")
    .order_by(lambda o: o.created_at, desc=True)
    .limit(20)
    .list()
)

Every method call has full type hints. IDEs auto-complete, and mypy catches errors at compile time.

#3.4 Automatic Permission Binding

Permissions are part of the Schema, not manually bound:

YAML
ObjectType: TransferOrder
  Actions:
    approve:
      permissions:
        - role: WarehouseManager
          condition: object.sourceWarehouse.manager == caller
        - role: SupplyChainDirector
          condition: always

Permission checks happen automatically when gRPC endpoints are called, fundamentally eliminating "forgot to add permission check" vulnerabilities.

#Part 4: Solving REST's Three Flaws

#4.1 Goodbye Endpoint Explosion

Adding business capabilities = adding ObjectTypes or Actions to Schema. The system auto-generates all interfaces. Growing from 10 to 100 entity types means managing 100 Schema definitions, not 3,000 endpoints.

#4.2 Goodbye N+1 Queries

Cross-entity queries are first-class citizens. Queries are translated into optimized execution plans. The Ontology layer knows entity relationships and selects optimal query paths automatically.

#4.3 Goodbye Version Hell

Schema version changes are auto-analyzed as "backward compatible" or "breaking." Compatible changes auto-deploy; breaking changes require explicit migration plans.

#Part 5: Practical Guide

#5.1 Start from Domain Models

Code
Traditional: Requirements -> API design -> Implementation -> Docs
OaaA: Requirements -> Domain modeling -> Ontology Schema -> Auto-generate everything

Design principles: derived properties auto-compute, state machines define valid transitions, relationships are bidirectional.

#5.2 Action Design Best Practices

  1. Express business intent, not technical operations
  2. Atomic — all-or-nothing
  3. Declare side effects — notifications, auditing, workflow triggers happen automatically

#5.3 Schema Evolution Strategies

  • Adding properties: Always safe
  • Removing properties: Two-step — deprecate first, remove after deadline
  • Modifying relationships: Use migration tools for validation and rollback

#Part 6: Performance Considerations

#6.1 Three-Level Caching

Code
L1: In-process cache (HashMap, TTL=60s)
L2: Distributed cache (Redis, TTL=5min)
L3: Schema Registry (PostgreSQL)

Schema changes are broadcast via event bus as invalidation notifications.

#6.2 Query Optimization

The query optimizer selects optimal execution plans based on data distribution and indexing — potentially SQL JOINs or graph traversals.

#6.3 Synergy with gRPC

Protobuf definitions map 1:1 to Ontology Schema. Server Streaming supports subscriptions. Binary serialization is 3-10x smaller than JSON.

#Part 7: Synergy with Other Patterns

PatternSynergy
State Machine (S10-05)Action state transitions defined in Schema
Cascade (S10-08)Derived property dependencies declared in Schema
Query Rewrite (S10-09)Permission rules derived from Schema
Contract-First (S10-12)Schema is upstream source for proto files
Multi-Tenancy (S10-14)Tenant isolation policies defined in Schema

#Part 8: Anti-Patterns and Pitfalls

#8.1 Over-Modeling

Not all data needs to enter the Ontology. Rule of thumb: if business users operate on it directly, it is an ObjectType. If only developers use it in code, it is an internal data structure.

#8.2 Everything as an Action

Simple property modifications should use standard Update. Actions are reserved for business-meaningful operations involving state transitions, permission checks, and side effects.

#8.3 Neglecting Relationship Design

Relationships are the soul of Ontology. Few inter-ObjectType relationships suggest REST-style thinking in Ontology design.

#Part 9: Case Study — Supply Chain Decision Platform

A manufacturing company's supply chain decision platform:

YAML
ObjectTypes:
  Supplier:
    properties: { name: String, rating: Decimal, leadTime: Duration }
    derived:
      onTimeRate: Decimal = orders.count(onTime) / orders.count()

  Warehouse:
    properties: { name: String, location: GeoPoint, capacity: Integer }
    derived:
      utilizationRate: Decimal = inventory.sum(quantity) / capacity
      criticalItems: List<InventoryItem> = inventory.filter(needsReorder)
    actions:
      requestRestock: { items, urgency } -> List<PurchaseOrder>

  InventoryItem:
    properties: { quantity: Integer, reorderPoint: Integer }
    derived:
      needsReorder: Boolean = quantity <= reorderPoint

SDK usage:

Python
platform = OntoPlatform(endpoint="grpc://control-Layer:9090")

critical = platform.objects.Warehouse.where(
    lambda w: w.critical_items.count() > 0
).list()

for wh in critical:
    wh.request_restock(items=wh.critical_items, urgency="Urgent")

Zero REST calls. Fully type-safe. Automatic permission checks.

#Part 10: Summary

Ontology-as-API is coomia-dip's most fundamental design decision:

  1. Endpoint explosion -> Schema definitions replace endpoint lists
  2. N+1 queries -> Relationship-aware query engine
  3. Version hell -> Schema evolution replaces API version management

You no longer design APIs — you design business models. APIs are projections of models.

#References

  1. Palantir Foundry Ontology Documentation
  2. coomia-dip Architecture Overview
  3. gRPC Official Documentation
  4. Eric Evans, Domain-Driven Design, Addison-Wesley, 2003

Next: S10-02 Strategy Routing Pattern