Back to Blog

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:

Coomia DIPPublished on December 27, 20255 min read
Share this articleTwitter / X

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:

Code
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:

PROTOBUF
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

Code
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

Python
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

Python
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:

YAML
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

Python
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

Python
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

Python
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

  1. Interface First: Define Protobuf/OpenAPI contracts before writing implementation code
  2. Compatibility Guarantees: CI/CD pipelines automatically check backward/forward contract compatibility
  3. Multi-Layer Contracts: gRPC contracts (inter-Layer), REST contracts (external API), Schema contracts (Ontology models)
  4. Contract Testing: Consumer-driven contract testing ensures providers meet consumer expectations
  5. Automatic Code Generation: Multi-language client and server code generated automatically from contract definitions
  6. 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