Back to Blog

Facade Pattern: Unified API Entry Point

coomia-dip internally has 8 Layers, each with its own gRPC services, data models, and communication protocols. If clients interacted directly with each Layer, complexity would be unmanageable:

Coomia DIPPublished on December 26, 20257 min read
Share this articleTwitter / X

Facade Pattern: Unified API Entry Point

“

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

#TL;DR

  • The Facade Pattern provides a unified API entry point for coomia-dip's multi-Layer architecture. External clients interact with a single API Gateway without needing to understand the complex topology of 8 internal Layers.
  • The coomia-dip facade layer implements cross-cutting concerns including protocol translation (REST to gRPC), request routing, authentication, rate limiting, circuit breaking, request aggregation, and response transformation.
  • The SDK is the client-side manifestation of the Facade Pattern — encapsulating complex multi-step API calls into simple method invocations.

#Introduction: Hiding Complexity

coomia-dip internally has 8 Layers, each with its own gRPC services, data models, and communication protocols. If clients interacted directly with each Layer, complexity would be unmanageable:

Code
Problem 1: Clients need to know each Layer's address and port
Problem 2: Different Layers use different Protobuf definitions
Problem 3: A single business operation may need to call 3-4 Layers
Problem 4: Auth, rate limiting, logging must be implemented in every Layer
Problem 5: Client upgrades need synchronized changes across multiple Layer calls

The Facade Pattern's core idea: hide internal system complexity behind a simple interface.

#Part 1: API Gateway Facade

#1.1 Unified Entry Architecture

Python
from dataclasses import dataclass, field
from typing import Any
from enum import Enum

class PlaneTarget(Enum):
    CONTROL = "control-Layer"
    DATA = "data-Layer"
    INTELLIGENCE = "intelligence-Layer"
    AGENT = "agent-runtime"
    SDK = "sdk-Layer"

@dataclass
class RouteConfig:
    path: str
    method: str
    target_plane: PlaneTarget
    grpc_service: str
    grpc_method: str
    requires_auth: bool = True
    rate_limit: int = 100
    timeout_seconds: int = 30
    cache_ttl: int = 0
    aggregate_from: list[str] = field(default_factory=list)

class APIGatewayFacade:
    """Unified API Gateway facade for coomia-dip."""

    def __init__(self, routes, auth_service, rate_limiter, circuit_breaker):
        self._routes = {(r.path, r.method): r for r in routes}
        self._auth = auth_service
        self._rate_limiter = rate_limiter
        self._circuit_breaker = circuit_breaker

    async def handle_request(self, request: "HTTPRequest") -> "HTTPResponse":
        route = self._routes.get((request.path, request.method))
        if not route:
            return HTTPResponse(status=404, body={"error": "Not found"})

        if route.requires_auth:
            auth_result = await self._auth.authenticate(request)
            if not auth_result.authenticated:
                return HTTPResponse(status=401, body={"error": "Unauthorized"})

        if not await self._rate_limiter.allow(request.client_id, route.rate_limit):
            return HTTPResponse(status=429, body={"error": "Rate limit exceeded"})

        if self._circuit_breaker.is_open(route.target_plane.value):
            return HTTPResponse(
                status=503,
                body={"error": f"Service {route.target_plane.value} unavailable"},
            )

        try:
            if route.aggregate_from:
                result = await self._aggregate_request(request, route)
            else:
                result = await self._route_request(request, route)
            return HTTPResponse(status=200, body=result)
        except TimeoutError:
            return HTTPResponse(status=504, body={"error": "Gateway timeout"})
        except Exception as e:
            self._circuit_breaker.record_failure(route.target_plane.value)
            return HTTPResponse(status=500, body={"error": str(e)})

    async def _route_request(self, request, route):
        endpoint = self._service_discovery.resolve(route.target_plane.value)
        async with grpc_channel(endpoint) as channel:
            grpc_request = self._rest_to_grpc(
                request.body, route.grpc_service, route.grpc_method
            )
            stub = self._get_stub(channel, route.grpc_service)
            method = getattr(stub, route.grpc_method)
            response = await asyncio.wait_for(
                method(grpc_request), timeout=route.timeout_seconds,
            )
            return self._grpc_to_rest(response)

    async def _aggregate_request(self, request, route):
        tasks = [
            self._call_plane(
                self._service_discovery.resolve(p), request, route
            )
            for p in route.aggregate_from
        ]
        results = await asyncio.gather(*tasks, return_exceptions=True)
        aggregated = {}
        for plane_name, result in zip(route.aggregate_from, results):
            aggregated[plane_name] = (
                {"error": str(result)} if isinstance(result, Exception) else result
            )
        return aggregated

#1.2 Route Configuration

Python
ROUTES = [
    RouteConfig("/api/v1/ontology/types", "GET", PlaneTarget.CONTROL,
                "OntologyService", "ListObjectTypes"),
    RouteConfig("/api/v1/ontology/types", "POST", PlaneTarget.CONTROL,
                "OntologyService", "CreateObjectType"),
    RouteConfig("/api/v1/ontology/objects/{type}", "GET", PlaneTarget.DATA,
                "ObjectQueryService", "QueryObjects"),
    RouteConfig("/api/v1/actions/{action_id}/execute", "POST", PlaneTarget.CONTROL,
                "ActionService", "ExecuteAction", timeout_seconds=60),
    RouteConfig("/api/v1/reasoning/evaluate", "POST", PlaneTarget.INTELLIGENCE,
                "ReasoningService", "Evaluate", timeout_seconds=120),
    RouteConfig("/api/v1/dashboard/overview", "GET", PlaneTarget.CONTROL,
                "DashboardService", "GetOverview",
                aggregate_from=["control-Layer", "data-Layer", "intelligence-Layer"]),
]

#Part 2: Protocol Translation Layer

#2.1 REST to gRPC Translation

Python
class ProtocolTranslator:
    def __init__(self, proto_registry: "ProtoRegistry"):
        self._proto_registry = proto_registry

    def rest_to_grpc(self, json_body: dict, service_name: str, method_name: str):
        message_type = self._proto_registry.get_request_type(service_name, method_name)
        return self._json_to_proto(json_body, message_type)

    def grpc_to_rest(self, proto_message) -> dict:
        return self._proto_to_json(proto_message)

    def _json_to_proto(self, data, message_type):
        from google.protobuf.json_format import ParseDict
        return ParseDict(data, message_type())

    def _proto_to_json(self, message) -> dict:
        from google.protobuf.json_format import MessageToDict
        return MessageToDict(message, preserving_proto_field_name=True)

#2.2 GraphQL Facade

In addition to REST, coomia-dip provides a GraphQL interface as another facade:

Python
class GraphQLFacade:
    async def execute_query(
        self, query: str, variables: dict | None = None, context: dict | None = None
    ) -> dict:
        parsed = self._parser.parse(query)
        execution_plan = self._planner.plan(parsed, variables)
        results = {}
        for step in execution_plan.steps:
            results[step.field_name] = await self._execute_step(step, context)
        return self._assemble_response(results, parsed)

#Part 3: Request Aggregation

#3.1 BFF (Backend For Frontend) Pattern

Different frontend pages need data from different Layers. The BFF layer aggregates multi-Layer data into a single response:

Python
class DashboardBFF:
    async def get_dashboard_data(self, tenant_id: str, world_id: str) -> dict:
        ontology_stats, data_stats, reasoning_stats, agent_stats = (
            await asyncio.gather(
                self._control_plane.get_ontology_stats(tenant_id),
                self._data_plane.get_storage_stats(tenant_id),
                self._intelligence_plane.get_reasoning_stats(tenant_id),
                self._agent_runtime.get_agent_stats(tenant_id),
            )
        )
        return {
            "ontology": {
                "total_types": ontology_stats.total_types,
                "total_objects": ontology_stats.total_objects,
                "total_links": ontology_stats.total_links,
            },
            "data": {
                "storage_used_gb": data_stats.storage_used_gb,
                "total_tables": data_stats.total_tables,
                "query_count_today": data_stats.query_count_today,
            },
            "reasoning": {
                "active_rules": reasoning_stats.active_rules,
                "decisions_today": reasoning_stats.decisions_today,
                "avg_latency_ms": reasoning_stats.avg_latency_ms,
            },
            "agents": {
                "active_agents": agent_stats.active_agents,
                "tasks_completed_today": agent_stats.tasks_completed_today,
            },
        }

#3.2 Batch Requests

Clients can send multiple API calls in a single HTTP request:

Python
class BatchRequestHandler:
    async def handle_batch(self, batch: list[dict], context: dict) -> list[dict]:
        independent, dependent = self._analyze_dependencies(batch)
        independent_results = await asyncio.gather(
            *[self._execute_single(req, context) for req in independent]
        )
        results = list(independent_results)
        for req in dependent:
            resolved = self._resolve_references(req, results)
            result = await self._execute_single(resolved, context)
            results.append(result)
        return results

#Part 4: SDK as Client-Side Facade

#4.1 SDK Encapsulation

The SDK is the client-side manifestation of the Facade Pattern. Users don't need to know how many APIs are called behind the scenes:

Python
class OntoPlatformSDK:
    def __init__(self, endpoint: str, api_key: str):
        self._client = HTTPClient(endpoint, api_key)

    async def create_risk_rule(
        self, name: str, conditions: list[dict], actions: list[dict]
    ) -> "RiskRule":
        """Create a risk rule — hides 4 API calls behind one method."""
        obj_type = await self._ensure_object_type("RiskRule")
        action = await self._client.post("/api/v1/actions", {
            "name": f"evaluate_{name}",
            "object_type": "RiskRule",
            "logic": conditions,
        })
        rule = await self._client.post("/api/v1/ontology/objects/RiskRule", {
            "name": name, "conditions": conditions,
            "actions": actions, "action_id": action["id"], "_state": "draft",
        })
        await self._client.post("/api/v1/reasoning/templates", {
            "rule_id": rule["id"], "conditions": conditions,
        })
        return RiskRule.from_dict(rule)

#4.2 SDK Error Translation

Python
class SDKErrorTranslator:
    ERROR_MAP = {
        "PERMISSION_DENIED": PermissionError,
        "NOT_FOUND": ObjectNotFoundError,
        "ALREADY_EXISTS": DuplicateObjectError,
        "INVALID_ARGUMENT": ValidationError,
        "RESOURCE_EXHAUSTED": QuotaExceededError,
        "UNAVAILABLE": ServiceUnavailableError,
    }

    def translate(self, error: Exception) -> Exception:
        if hasattr(error, "code"):
            error_class = self.ERROR_MAP.get(error.code().name, SDKError)
            return error_class(str(error.details()))
        elif hasattr(error, "status_code"):
            if error.status_code == 404:
                return ObjectNotFoundError(error.text)
            elif error.status_code == 403:
                return PermissionError(error.text)
        return SDKError(str(error))

#Part 5: Cross-Cutting Concerns

#5.1 Request Logging and Tracing

Python
class RequestLogger:
    async def log_request(self, request, response, duration_ms: float) -> None:
        await self._log_store.append({
            "timestamp": datetime.utcnow().isoformat(),
            "method": request.method, "path": request.path,
            "client_id": request.client_id, "status": response.status,
            "duration_ms": duration_ms,
            "trace_id": request.headers.get("X-Trace-ID"),
        })

#5.2 API Versioning

Python
class APIVersionRouter:
    def __init__(self):
        self._versions: dict[str, dict] = {}

    def register_version(self, version: str, routes: list[RouteConfig]) -> None:
        self._versions[version] = {(r.path, r.method): r for r in routes}

    def resolve(self, version: str, path: str, method: str) -> RouteConfig | None:
        return self._versions.get(version, {}).get((path, method))

#5.3 Response Caching

Python
class ResponseCache:
    async def get_or_execute(self, cache_key: str, ttl: int, executor: callable) -> dict:
        cached = await self._redis.get(f"api:cache:{cache_key}")
        if cached:
            return json.loads(cached)
        result = await executor()
        if ttl > 0:
            await self._redis.set(f"api:cache:{cache_key}", json.dumps(result), ex=ttl)
        return result

#Key Takeaways

  1. Complexity Hiding: The Facade Pattern hides 8 Layers' complex topology behind a unified API
  2. Protocol Translation: Transparent REST/JSON to gRPC/Protobuf conversion
  3. Request Aggregation: BFF layer aggregates multi-Layer data into single responses, reducing client round-trips
  4. SDK Encapsulation: SDK wraps multi-step API calls into single methods with business-friendly interfaces
  5. Cross-Cutting Concerns: Auth, rate limiting, circuit breaking, logging, and caching handled uniformly at the facade
  6. Version Management: API version routing ensures backward compatibility

#Next Article

In the next article, we will explore the Contract-First pattern — how coomia-dip uses Protobuf and OpenAPI definition-first approaches to ensure cross-Layer interface compatibility.

S10-12: Contract-First: Interface Definition First

#Tags

#DesignPatterns #FacadePattern #APIGateway #ProtocolTranslation #BFF #SDK #RateLimiting #CircuitBreaker