Skip to main content

ADR-004: Gateway Adapter Pattern β€” Multi-Gateway Orchestration

Metadata​

FieldValue
StatusAccepted
Date2026-02-06
LinearN/A (Foundational)
ResearchCIR-eligible: Declarative Reconciliation of Proprietary API Gateways

Context​

STOA Platform needs to orchestrate multiple API gateways (webMethods, Kong, Apigee, AWS API Gateway) through a unified control plane. Each gateway has a different admin API, different authentication mechanisms, and different resource models.

The Problem​

"How do we manage APIs across heterogeneous gateways without creating N different codebases?"

Traditional approaches either:

  1. Vendor lock-in β€” Build tight coupling to one gateway
  2. Lowest common denominator β€” Support only features common to all gateways
  3. Code duplication β€” Implement separate integrations per gateway

STOA needs an abstraction that enables:

  • Full feature utilization per gateway
  • Unified GitOps reconciliation
  • Testable adapter implementations
  • Future gateway additions without control plane changes

Research Context​

This pattern emerged from research into declarative reconciliation of proprietary API gateways β€” specifically webMethods, which lacks both a Terraform provider and a Kubernetes operator. The solution represents original research in applying GitOps principles to closed-source enterprise software.

Decision​

Adopt the Gateway Adapter Pattern β€” an abstract interface defining all operations required for STOA's GitOps reconciliation, with concrete implementations per gateway type.

Architecture​

Interface Contract​

class GatewayAdapterInterface(ABC):
"""Abstract interface for gateway orchestration.

All operations MUST be idempotent: calling the same operation twice
with the same input must produce the same result without side effects.
"""

# --- Lifecycle (3 methods) ---
async def health_check(self) -> AdapterResult: ...
async def connect(self) -> None: ...
async def disconnect(self) -> None: ...

# --- APIs (3 methods) ---
async def sync_api(self, api_spec: dict, tenant_id: str) -> AdapterResult: ...
async def delete_api(self, api_id: str) -> AdapterResult: ...
async def list_apis(self) -> list[dict]: ...

# --- Policies (3 methods) ---
async def upsert_policy(self, policy_spec: dict) -> AdapterResult: ...
async def delete_policy(self, policy_id: str) -> AdapterResult: ...
async def list_policies(self) -> list[dict]: ...

# --- Applications (3 methods) ---
async def provision_application(self, app_spec: dict) -> AdapterResult: ...
async def deprovision_application(self, app_id: str) -> AdapterResult: ...
async def list_applications(self) -> list[dict]: ...

# --- Auth/OIDC (3 methods) ---
async def upsert_auth_server(self, auth_spec: dict) -> AdapterResult: ...
async def upsert_strategy(self, strategy_spec: dict) -> AdapterResult: ...
async def upsert_scope(self, scope_spec: dict) -> AdapterResult: ...

# --- Aliases (1 method) ---
async def upsert_alias(self, alias_spec: dict) -> AdapterResult: ...

# --- Configuration (1 method) ---
async def apply_config(self, config_spec: dict) -> AdapterResult: ...

# --- Backup (1 method) ---
async def export_archive(self) -> bytes: ...

Standardized Result​

All operations return a consistent result type:

@dataclass
class AdapterResult:
success: bool
resource_id: str | None = None # Gateway-side ID
data: dict | None = None # Additional response data
error: str | None = None # Error message if failed

Adapter Registry​

Factory pattern for creating adapter instances:

class AdapterRegistry:
_adapters: dict[str, type[GatewayAdapterInterface]] = {}

@classmethod
def register(cls, gateway_type: str, adapter_class: type) -> None:
cls._adapters[gateway_type] = adapter_class

@classmethod
def create(cls, gateway_type: str, config: dict) -> GatewayAdapterInterface:
adapter_class = cls._adapters.get(gateway_type)
if not adapter_class:
raise ValueError(f"No adapter for '{gateway_type}'")
return adapter_class(config=config)

Registered Adapters​

TypeClassStatusUse Case
webmethodsWebMethodsGatewayAdapterProductionEnterprise gateway
stoaStoaGatewayAdapterProductionInternal/MCP gateway
templateTemplateGatewayAdapterScaffoldNew adapter development
kong(planned)Q2 2026OSS gateway
apigee(planned)Q3 2026Google Cloud

Implementation Structure​

control-plane-api/src/adapters/
β”œβ”€β”€ __init__.py
β”œβ”€β”€ gateway_adapter_interface.py # Abstract interface
β”œβ”€β”€ registry.py # Adapter factory
β”‚
β”œβ”€β”€ webmethods/ # Production adapter
β”‚ β”œβ”€β”€ __init__.py
β”‚ β”œβ”€β”€ adapter.py # WebMethodsGatewayAdapter
β”‚ β”œβ”€β”€ client.py # HTTP client for webMethods API
β”‚ └── mappers.py # Spec <-> webMethods translation
β”‚
β”œβ”€β”€ stoa/ # Internal adapter
β”‚ β”œβ”€β”€ __init__.py
β”‚ └── adapter.py # StoaGatewayAdapter
β”‚
└── template/ # Scaffold for new adapters
β”œβ”€β”€ __init__.py
β”œβ”€β”€ adapter.py # TemplateGatewayAdapter
└── mappers.py # TODO comments for implementation

Idempotency Requirements​

All adapter operations MUST be idempotent:

OperationIdempotency Behavior
sync_apiCreates if missing, updates if exists (upsert)
delete_apiNo-op if already deleted
upsert_policyCreates or updates based on policy ID
provision_applicationReturns existing app ID if already provisioned
health_checkSafe to call repeatedly

Idempotent Reconciliation Loop​

async def reconcile_api(adapter: GatewayAdapterInterface, api_spec: dict):
"""Idempotent reconciliation β€” safe to retry on failure."""

# 1. Check current state
existing = await adapter.list_apis()

# 2. Sync desired state (creates or updates)
result = await adapter.sync_api(api_spec, tenant_id)

if not result.success:
# Log and retry β€” idempotent, so safe
logger.warning(f"Sync failed: {result.error}")
raise ReconciliationError(result.error)

return result.resource_id

GitOps Integration​

The adapter pattern enables declarative GitOps for any gateway:

# stoa-gitops/tenants/acme/apis/payment-api/api.yaml
apiVersion: gostoa.dev/v1alpha1
kind: API
metadata:
name: payment-api
tenant: acme
spec:
gateway_type: webmethods # Selects adapter
openapi: ./openapi.yaml
policies:
- rate-limit-100rpm
- jwt-validation

The reconciliation pipeline:

  1. Detects change in Git
  2. Loads API spec from YAML
  3. Uses AdapterRegistry.create("webmethods") to get adapter
  4. Calls adapter.sync_api(spec, "acme")
  5. Records result in audit log

Consequences​

Positive​

  • Gateway Agnostic β€” Control plane doesn't know gateway internals
  • Testable β€” Mock adapters for unit tests, real adapters for integration
  • Extensible β€” New gateway support without control plane changes
  • Idempotent β€” Safe retries, GitOps-compatible
  • Vendor Neutral β€” Avoid lock-in to any single gateway

Negative​

  • Abstraction Overhead β€” Some gateway-specific features may not fit the interface
  • Mapping Complexity β€” Each adapter must translate to/from gateway-specific formats
  • Version Skew β€” Gateway API versions may drift across implementations

Mitigations​

ChallengeMitigation
Feature gapsdata field in AdapterResult carries gateway-specific extras
Mapping complexityDedicated mappers.py module per adapter
Version skewAdapter tests run against specific gateway versions

Adding a New Gateway​

To add Kong support:

  1. Copy template/ to kong/
  2. Implement KongGatewayAdapter following mappers.py TODOs
  3. Register in registry.py:
    from .kong import KongGatewayAdapter
    AdapterRegistry.register("kong", KongGatewayAdapter)
  4. Add integration tests against Kong Admin API
  5. Update documentation

References​


Standard Marchemalo: A 40-year veteran architect understands in 30 seconds