Contract-First: Interface Definition First
In multi-team development, a typical problem arises: Team A develops the Control Layer, Team B develops the Data Layer, and they communicate via gRPC. Without pre-agreed interface definitions:
Contract-First: Interface Definition First
“Series: S10 Design Patterns · Article 12 | Level: Advanced | Reading Time: 18 min
#TL;DR
- The Contract-First pattern requires defining interface contracts before writing implementation code. coomia-dip uses Protobuf for gRPC service contracts, OpenAPI for REST API contracts, and JSON Schema for Ontology Schema contracts.
- Contracts are collaboration boundaries between Layers — as long as the contract remains stable, each Layer can be developed, tested, and deployed independently.
- coomia-dip implements contract compatibility checking (backward/forward compatible), contract version management, and automatic code generation, ensuring multi-team collaboration stability.
#Introduction: Interface First or Implementation First
In multi-team development, a typical problem arises: Team A develops the Control Layer, Team B develops the Data Layer, and they communicate via gRPC. Without pre-agreed interface definitions:
Team A assumes CreateObject returns object_id: string
Team B assumes CreateObject returns id: int64
→ Incompatibility discovered during integration, requiring rework
The Contract-First pattern's core: define the interface first, then write the implementation. The interface definition is the collaboration contract between teams.
#Part 1: Protobuf as gRPC Contract
#1.1 Inter-Layer gRPC Service Definitions
All coomia-dip inter-Layer communication uses Protobuf-defined gRPC services:
syntax = "proto3";
package onto.ontology.v1;
import "google/protobuf/timestamp.proto";
import "google/protobuf/struct.proto";
service OntologyService {
rpc CreateObjectType(CreateObjectTypeRequest) returns (CreateObjectTypeResponse);
rpc GetObjectType(GetObjectTypeRequest) returns (ObjectType);
rpc ListObjectTypes(ListObjectTypesRequest) returns (ListObjectTypesResponse);
rpc UpdateObjectType(UpdateObjectTypeRequest) returns (ObjectType);
rpc DeleteObjectType(DeleteObjectTypeRequest) returns (DeleteObjectTypeResponse);
rpc CreateLinkType(CreateLinkTypeRequest) returns (CreateLinkTypeResponse);
rpc RegisterAction(RegisterActionRequest) returns (RegisterActionResponse);
rpc ExecuteAction(ExecuteActionRequest) returns (ExecuteActionResponse);
}
message CreateObjectTypeRequest {
string name = 1;
string display_name = 2;
string description = 3;
google.protobuf.Struct schema = 4;
string tenant_id = 5;
string world_id = 6;
}
message CreateObjectTypeResponse {
string type_id = 1;
int32 version = 2;
google.protobuf.Timestamp created_at = 3;
}
message ObjectType {
string type_id = 1;
string name = 2;
string display_name = 3;
string description = 4;
google.protobuf.Struct schema = 5;
int32 version = 6;
string tenant_id = 7;
string world_id = 8;
google.protobuf.Timestamp created_at = 9;
google.protobuf.Timestamp updated_at = 10;
}
#1.2 Contract Directory Structure
proto/
├── ontology/v1/ # Ontology service definitions
├── data/v1/ # Data Layer service definitions
├── reasoning/v1/ # Reasoning service definitions
├── agent/v1/ # Agent service definitions
└── common/v1/ # Shared error, pagination, metadata
#Part 2: Contract Compatibility Management
#2.1 Compatibility Rules
class CompatibilityChecker:
def check_backward_compatibility(self, old_proto: str, new_proto: str) -> list[str]:
violations = []
old_desc = self._parse(old_proto)
new_desc = self._parse(new_proto)
# Rule 1: Cannot remove existing fields
for field in old_desc.fields:
if field.number not in [f.number for f in new_desc.fields]:
violations.append(f"Field {field.name} (number {field.number}) was removed")
# Rule 2: Cannot change existing field types
for old_field in old_desc.fields:
for new_field in new_desc.fields:
if old_field.number == new_field.number:
if old_field.type != new_field.type:
violations.append(
f"Field {old_field.name}: type changed from {old_field.type} to {new_field.type}"
)
# Rule 3: Cannot remove RPC methods
for rpc in old_desc.services[0].methods:
if rpc.name not in [m.name for m in new_desc.services[0].methods]:
violations.append(f"RPC method {rpc.name} was removed")
return violations
#2.2 Contract Checks in CI/CD
class ContractCICheck:
async def run_checks(self, pr_branch: str, main_branch: str) -> dict:
changed_protos = await self._git.diff_files(main_branch, pr_branch, pattern="*.proto")
results = {"passed": True, "violations": []}
for proto_path in changed_protos:
old_content = await self._git.get_file(main_branch, proto_path)
new_content = await self._git.get_file(pr_branch, proto_path)
if old_content:
violations = self._checker.check_backward_compatibility(old_content, new_content)
if violations:
results["passed"] = False
results["violations"].extend([
{"file": proto_path, "violation": v} for v in violations
])
return results
#Part 3: OpenAPI Contract
#3.1 REST API Definition
Externally exposed REST APIs use OpenAPI 3.0:
openapi: 3.0.3
info:
title: coomia-dip Ontology API
version: 1.0.0
paths:
/api/v1/ontology/types:
get:
operationId: listObjectTypes
summary: List all ObjectTypes
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/ListObjectTypesResponse'
post:
operationId: createObjectType
summary: Create a new ObjectType
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateObjectTypeRequest'
#Part 4: Ontology Schema Contract
#4.1 JSON Schema for ObjectType Definition
class OntologySchemaContract:
OBJECT_TYPE_META_SCHEMA = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["name", "properties"],
"properties": {
"name": {"type": "string", "pattern": "^[A-Z][a-zA-Z0-9]+$"},
"properties": {
"type": "object",
"additionalProperties": {"$ref": "#/$defs/PropertyDefinition"},
},
},
}
async def validate_schema(self, schema: dict) -> list[str]:
import jsonschema
errors = []
try:
jsonschema.validate(schema, self.OBJECT_TYPE_META_SCHEMA)
except jsonschema.ValidationError as e:
errors.append(str(e.message))
return errors
async def check_evolution_compatibility(self, old_schema: dict, new_schema: dict) -> list[str]:
errors = []
old_props = old_schema.get("properties", {})
new_props = new_schema.get("properties", {})
removed = set(old_props) - set(new_props)
if removed:
errors.append(f"Properties removed: {removed}")
for name in set(old_props) & set(new_props):
if old_props[name].get("type") != new_props[name].get("type"):
errors.append(f"Property '{name}' type changed")
for name in set(new_props) - set(old_props):
if new_props[name].get("required") and "default" not in new_props[name]:
errors.append(f"New required property '{name}' must have a default value")
return errors
#Part 5: Contract Testing
#5.1 Consumer-Driven Contract Testing
class ContractTest:
async def verify_provider(self, provider_name: str, contract: dict) -> dict:
results = {"passed": True, "failures": []}
for interaction in contract["interactions"]:
response = await self._client.request(
method=interaction["request"]["method"],
path=interaction["request"]["path"],
body=interaction["request"].get("body"),
)
expected = interaction["response"]
if response.status != expected["status"]:
results["passed"] = False
results["failures"].append({
"interaction": interaction["description"],
"expected_status": expected["status"],
"actual_status": response.status,
})
if "body" in expected:
if not self._validate_response_schema(response.body, expected["body"]):
results["passed"] = False
results["failures"].append({
"interaction": interaction["description"],
"error": "Response body schema mismatch",
})
return results
#Part 6: Automatic Code Generation
#6.1 Multi-Language Code Generation from Protobuf
class MultiLanguageCodeGen:
GENERATORS = {
"python": "grpcio-tools",
"java": "protoc-gen-grpc-java",
"typescript": "protoc-gen-ts",
}
async def generate_all(self, proto_dir: str, output_dir: str) -> dict:
results = {}
for lang, tool in self.GENERATORS.items():
result = await self._generate(proto_dir, output_dir, lang, tool)
results[lang] = result
return results
#Key Takeaways
- Interface First: Define Protobuf/OpenAPI contracts before writing implementation code
- Compatibility Guarantees: CI/CD pipelines automatically check backward/forward contract compatibility
- Multi-Layer Contracts: gRPC contracts (inter-Layer), REST contracts (external API), Schema contracts (Ontology models)
- Contract Testing: Consumer-driven contract testing ensures providers meet consumer expectations
- Automatic Code Generation: Multi-language client and server code generated automatically from contract definitions
- Schema Evolution: Ontology Schema changes must pass compatibility checks
#Next Article
S10-13: Auto-Provisioning Pattern: Resources Created on Demand
#Tags
#DesignPatterns #ContractFirst #Protobuf #OpenAPI #gRPC #Compatibility #CodeGeneration #ContractTesting