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.
“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:
┌─────────────────────────────────────────┐
│ 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:
# 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:
# 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:
# 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:
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:
# 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 Dimension | Palantir OSDK | coomia-dip SDK |
|---|---|---|
| Core abstraction | Ontology objects | Ontology objects |
| Type safety | TypeScript generated | Python + TS generated |
| Communication | REST/JSON | gRPC/Protobuf |
| Code generation | CLI tool | CLI + CI integration |
| Async support | Promise | Native async/await |
#2. Layered Architecture
#2.1 Four-Layer SDK Architecture
┌─────────────────────────────────────────┐
│ 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
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
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
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
# 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
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
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
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
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
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
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
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:
- Ontology-First: Developers work with object models, not low-level APIs
- Type-Safe: Code generation ensures compile-time type checking
- Four-layer architecture: Foundation, Transport, Domain, Generated
- Fail-Fast: Clear error hierarchy with remediation suggestions
- Version compatible: Semantic versioning with backward compatibility guarantees
The next article will explore gRPC client code generation implementation in detail.