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:
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:
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
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
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
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:
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:
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:
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:
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
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
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
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
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
- Complexity Hiding: The Facade Pattern hides 8 Layers' complex topology behind a unified API
- Protocol Translation: Transparent REST/JSON to gRPC/Protobuf conversion
- Request Aggregation: BFF layer aggregates multi-Layer data into single responses, reducing client round-trips
- SDK Encapsulation: SDK wraps multi-step API calls into single methods with business-friendly interfaces
- Cross-Cutting Concerns: Auth, rate limiting, circuit breaking, logging, and caching handled uniformly at the facade
- 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