Back to Blog

SDK Design Philosophy: Ontology-First Developer Experience

The coomia-dip SDK design philosophy is "Ontology-First" -- developers interact with the platform through Ontology object models rather than low-level APIs. The SDK provides type-safe code generation, intuitive fluent APIs, automatic permission context propagation, and transparent gRPC communication. This article covers the complete design from principles, layered architecture, code generation strategy, error handling philosophy, to version compatibility.

CoomiaPublished on September 27, 20258 min read
Share this articleTwitter / X

Series: S6 Platform Engineering · Article 13 | Level: Advanced | Reading Time: 18 min

SDK Design Philosophy: Ontology-First Developer Experience

#TL;DR

The coomia-dip SDK design philosophy is "Ontology-First" -- developers interact with the platform through Ontology object models rather than low-level APIs. The SDK provides type-safe code generation, intuitive fluent APIs, automatic permission context propagation, and transparent gRPC communication. This article covers the complete design from principles, layered architecture, code generation strategy, error handling philosophy, to version compatibility.

#1. Design Principles

#1.1 Five Core Principles

The coomia-dip SDK design follows five core principles:

Code
┌─────────────────────────────────────────┐
│          SDK Design Principles           │
│                                          │
│  1. Ontology-First  (Ontology priority)  │
│  2. Type-Safe       (Compile-time safety)│
│  3. Zero-Boilerplate(No boilerplate)     │
│  4. Fail-Fast       (Early error detect) │
│  5. Transparent     (Observable comms)   │
└─────────────────────────────────────────┘

Principle 1: Ontology-First

Developers work with Ontology objects (Employee, Project, Action), not REST endpoints or gRPC methods. The SDK encapsulates platform capabilities behind the object model:

Python
# Anti-pattern: low-level API calls
response = client.post("/api/v1/objects/Employee/query", json={...})

# coomia-dip SDK: Ontology-First
employees = await client.Employee.where(
    Employee.department == "Engineering"
).select(
    Employee.name, Employee.salary
).limit(100).execute()

Principle 2: Type-Safe

All Ontology operations are type-checked at compile time. Property names, types, and constraints are embedded through code generation:

Python
# Type errors show immediately in IDE
employee.salary = "not_a_number"  # TypeError: expected int, got str
employee.nonexistent_field = 42   # AttributeError: Employee has no property 'nonexistent_field'

Principle 3: Zero-Boilerplate

Infrastructure code for connection setup, authentication, serialization, and error retries is handled automatically:

Python
# One-line initialization
client = OntoPlatform.connect("https://platform.example.com", token="...")

# No manual request construction or response parsing
employee = await client.Employee.get("emp-001")

Principle 4: Fail-Fast

Errors are detected and reported at the earliest possible point with clear messages and remediation suggestions:

Python
try:
    await client.Employee.create(name="John")  # Missing required field 'department'
except ValidationError as e:
    # "Employee.create requires field 'department' (str).
    #  Provided fields: ['name']. Missing required: ['department', 'hire_date']"

Principle 5: Transparent

Internal gRPC communication is transparent to developers but observable when needed:

Python
# Enable request logging
client = OntoPlatform.connect(..., debug=True)
# DEBUG: gRPC call OntologyService.GetObject(type=Employee, id=emp-001) -> 200 (12ms)

#1.2 Comparison with Palantir OSDK

Design DimensionPalantir OSDKcoomia-dip SDK
Core abstractionOntology objectsOntology objects
Type safetyTypeScript generatedPython + TS generated
CommunicationREST/JSONgRPC/Protobuf
Code generationCLI toolCLI + CI integration
Async supportPromiseNative async/await

#2. Layered Architecture

#2.1 Four-Layer SDK Architecture

Code
┌─────────────────────────────────────────┐
│  Layer 4: Generated Ontology Layer      │
│  (Auto-generated type-safe models)      │
│  Employee, Project, CreateEmployee...   │
├─────────────────────────────────────────┤
│  Layer 3: Domain API Layer              │
│  (Domain operation APIs)                │
│  ObjectClient, ActionClient,            │
│  SearchClient, LinkClient               │
├─────────────────────────────────────────┤
│  Layer 2: Transport Layer               │
│  (gRPC communication + serialization)   │
│  GrpcChannel, Interceptors,             │
│  Serializer, RetryPolicy                │
├─────────────────────────────────────────┤
│  Layer 1: Foundation Layer              │
│  (Infrastructure)                       │
│  Auth, Config, Logging,                 │
│  Connection Pool, Circuit Breaker       │
└─────────────────────────────────────────┘

#2.2 Layer 1: Foundation

Python
class SDKConfig(BaseModel):
    """SDK configuration"""

    platform_url: str = Field(description="Platform URL")
    auth_token: str | None = Field(default=None)
    auth_provider: AuthProvider | None = Field(default=None)

    max_connections: int = Field(default=10)
    connect_timeout_ms: int = Field(default=5000)
    request_timeout_ms: int = Field(default=30000)

    max_retries: int = Field(default=3)
    retry_backoff_ms: int = Field(default=100)
    retry_max_backoff_ms: int = Field(default=5000)

    enable_tracing: bool = Field(default=False)
    enable_metrics: bool = Field(default=False)
    log_level: str = Field(default="INFO")


class ConnectionManager:
    """Connection manager with connection pool and health checks"""

    def __init__(self, config: SDKConfig):
        self._config = config
        self._channel: grpc.aio.Channel | None = None
        self._circuit_breaker = CircuitBreaker(
            failure_threshold=5, recovery_timeout=30,
        )

    async def get_channel(self) -> grpc.aio.Channel:
        if self._channel is None:
            self._channel = grpc.aio.insecure_channel(
                self._config.platform_url,
                options=[
                    ("grpc.max_receive_message_length", 50 * 1024 * 1024),
                    ("grpc.keepalive_time_ms", 10000),
                ],
            )
        return self._channel

#2.3 Layer 2: Transport

Python
class AuthInterceptor(grpc.aio.UnaryUnaryClientInterceptor):
    """Authentication interceptor"""

    async def intercept_unary_unary(self, continuation, client_call_details, request):
        metadata = list(client_call_details.metadata or [])
        token = await self._auth_provider.get_token()
        metadata.append(("authorization", f"Bearer {token}"))
        new_details = client_call_details._replace(metadata=metadata)
        return await continuation(new_details, request)


class RetryInterceptor(grpc.aio.UnaryUnaryClientInterceptor):
    """Retry interceptor with exponential backoff"""

    RETRYABLE_CODES = {
        grpc.StatusCode.UNAVAILABLE,
        grpc.StatusCode.DEADLINE_EXCEEDED,
        grpc.StatusCode.RESOURCE_EXHAUSTED,
    }

    async def intercept_unary_unary(self, continuation, client_call_details, request):
        for attempt in range(self._max_retries + 1):
            try:
                return await continuation(client_call_details, request)
            except grpc.aio.AioRpcError as e:
                if e.code() not in self.RETRYABLE_CODES or attempt == self._max_retries:
                    raise
                await asyncio.sleep(self._backoff(attempt))


class TracingInterceptor(grpc.aio.UnaryUnaryClientInterceptor):
    """OpenTelemetry tracing interceptor"""

    async def intercept_unary_unary(self, continuation, client_call_details, request):
        with tracer.start_as_current_span(
            f"grpc.{client_call_details.method}", kind=SpanKind.CLIENT,
        ) as span:
            span.set_attribute("rpc.system", "grpc")
            span.set_attribute("rpc.method", client_call_details.method)
            try:
                response = await continuation(client_call_details, request)
                span.set_status(StatusCode.OK)
                return response
            except grpc.aio.AioRpcError as e:
                span.set_status(StatusCode.ERROR, str(e))
                raise

#2.4 Layer 3: Domain API

Python
class ObjectClient:
    """Object operations client"""

    async def get(self, object_type: str, object_id: str) -> OntologyObject: ...
    async def list(self, object_type: str, filters=None, order_by=None,
                   page_size=100, page_token=None) -> PagedResult[OntologyObject]: ...
    async def create(self, object_type: str, properties: dict) -> OntologyObject: ...
    async def update(self, object_type: str, object_id: str, updates: dict) -> OntologyObject: ...
    async def delete(self, object_type: str, object_id: str) -> None: ...


class ActionClient:
    """Action operations client"""

    async def execute(self, action_type: str, parameters: dict,
                      mode=ExecutionMode.VALIDATE_AND_EXECUTE) -> ActionResult: ...
    async def validate(self, action_type: str, parameters: dict) -> ValidationResult: ...

#2.5 Layer 4: Generated

Python
# Auto-generated type-safe model (example)
class Employee(OntologyObject):
    """Employee object type - auto-generated"""

    __object_type__ = "Employee"

    name: str
    department: str
    salary: int
    hire_date: date
    email: str | None = None
    manager_id: str | None = None

    class Properties:
        name = StringProperty("name")
        department = StringProperty("department")
        salary = IntProperty("salary")
        hire_date = DateProperty("hire_date")
        email = StringProperty("email")
        manager_id = StringProperty("manager_id")

    async def manager(self) -> "Employee | None":
        if self.manager_id:
            return await self._client.Employee.get(self.manager_id)
        return None

    async def reports(self) -> list["Employee"]:
        return await self._client.Employee.where(
            Employee.Properties.manager_id == self.id
        ).execute()

#3. Code Generation Strategy

#3.1 Generation Pipeline

Code
Schema Registry → Protobuf Def → Code Generator → Type-Safe SDK
       │               │                │               │
  Ontology Schema   .proto files    Jinja2 templates  Python/TS code
  (runtime fetch)   (intermediate)  (language-specific) (developer use)

#3.2 Generator Implementation

Python
class SDKCodeGenerator:
    """SDK code generator"""

    async def generate(
        self, schema_url: str, output_dir: str, language: str = "python",
    ) -> GenerationResult:
        schemas = await self._schema_client.list_object_types()

        generated_files = []
        for schema in schemas:
            template = self._get_template(language, schema.type)
            code = template.render(
                object_type=schema,
                properties=schema.properties,
                links=schema.links,
                actions=schema.actions,
            )
            file_path = f"{output_dir}/{schema.api_name.lower()}.py"
            generated_files.append(file_path)

        index_code = self._generate_index(schemas, language)
        generated_files.append(f"{output_dir}/__init__.py")

        return GenerationResult(
            files=generated_files, object_types=len(schemas), language=language,
        )

#4. Error Handling Philosophy

#4.1 Error Hierarchy

Python
class OntoSDKError(Exception):
    """SDK base error"""
    def __init__(self, message: str, code: str, details: dict | None = None):
        self.code = code
        self.details = details or {}
        super().__init__(message)

class ConnectionError(OntoSDKError): pass
class AuthenticationError(OntoSDKError): pass
class AuthorizationError(OntoSDKError): pass

class ValidationError(OntoSDKError):
    def __init__(self, message: str, field_errors: dict[str, list[str]]):
        self.field_errors = field_errors
        super().__init__(message, code="VALIDATION_ERROR")

class ObjectNotFoundError(OntoSDKError):
    def __init__(self, object_type: str, object_id: str):
        super().__init__(
            f"{object_type} with id '{object_id}' not found",
            code="NOT_FOUND",
            details={"object_type": object_type, "object_id": object_id},
        )

class ConflictError(OntoSDKError): pass

#4.2 Error Mapping

Python
class GrpcErrorMapper:
    """gRPC error code to SDK error mapping"""

    MAPPING = {
        grpc.StatusCode.NOT_FOUND: ObjectNotFoundError,
        grpc.StatusCode.PERMISSION_DENIED: AuthorizationError,
        grpc.StatusCode.UNAUTHENTICATED: AuthenticationError,
        grpc.StatusCode.INVALID_ARGUMENT: ValidationError,
        grpc.StatusCode.ALREADY_EXISTS: ConflictError,
        grpc.StatusCode.UNAVAILABLE: ConnectionError,
    }

    def map(self, grpc_error: grpc.aio.AioRpcError) -> OntoSDKError:
        error_class = self.MAPPING.get(grpc_error.code(), OntoSDKError)
        return error_class(message=grpc_error.details(), code=grpc_error.code().name)

#5. Version Compatibility

#5.1 Semantic Versioning

Code
SDK Version: MAJOR.MINOR.PATCH
  MAJOR: Incompatible API changes
  MINOR: Backward-compatible feature additions
  PATCH: Backward-compatible bug fixes

Schema Version: Independent incrementing
  SDK supports schema version ranges (e.g., v5-v8)

#5.2 Backward Compatibility Guarantee

Python
class VersionNegotiator:
    """Version negotiator"""

    async def negotiate(self, server_version: str) -> CompatibilityResult:
        sdk_version = self._get_sdk_version()

        if not self._is_compatible(sdk_version, server_version):
            return CompatibilityResult(
                compatible=False,
                message=f"SDK {sdk_version} not compatible with server {server_version}. "
                        f"Please upgrade to >= {self._min_required_sdk(server_version)}",
            )

        if self._has_deprecation_warnings(sdk_version, server_version):
            return CompatibilityResult(
                compatible=True,
                warnings=self._get_deprecation_warnings(sdk_version, server_version),
            )

        return CompatibilityResult(compatible=True)

#6. Testing Strategy

Python
class TestSDKDesign:
    async def test_ontology_first_api(self):
        client = OntoPlatform.connect(test_url, token=test_token)
        employee = await client.Employee.get("emp-001")
        assert isinstance(employee, Employee)
        assert hasattr(employee, "name")

    async def test_type_safety(self):
        with pytest.raises(ValidationError):
            await client.Employee.create(name="John", salary="not_a_number")

    async def test_error_mapping(self):
        with pytest.raises(ObjectNotFoundError) as exc_info:
            await client.Employee.get("nonexistent")
        assert "not found" in str(exc_info.value)

    async def test_retry_on_transient_error(self):
        mock_channel = MockGrpcChannel(failures=2)
        client = OntoPlatform(channel=mock_channel)
        result = await client.Employee.get("emp-001")
        assert result is not None
        assert mock_channel.call_count == 3

#7. Summary

The coomia-dip SDK design philosophy is built around five core principles, with each layer from low-level infrastructure to top-level type-safe models serving developer experience. Key design decisions:

  1. Ontology-First: Developers work with object models, not low-level APIs
  2. Type-Safe: Code generation ensures compile-time type checking
  3. Four-layer architecture: Foundation, Transport, Domain, Generated
  4. Fail-Fast: Clear error hierarchy with remediation suggestions
  5. Version compatible: Semantic versioning with backward compatibility guarantees

The next article will explore gRPC client code generation implementation in detail.