executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
ports-patternsDefines and manages port interfaces (driving/driven) in hexagonal architecture using Python Protocols, abc.ABC classes, and explicit contract patterns for framework-agnostic boundaries.
Independent GitHub snapshot · Automated classification · Original documentation governs
name: ports-patterns description: Defines and manages port interfaces (driving/driven) in hexagonal architecture using Python Protocols, abc.ABC classes, and explicit contract patterns for framework-agnostic boundaries. license: MIT compatibility: opencode metadata: version: "1.0.0" domain: coding triggers: port interface, driving port, driven port, port contract, Protocol vs ABC, hexagonal ports, how do i define clean boundaries, dependency inversion ABC archetypes:
Acts as a senior software architect designing clean port interfaces for hexagonal architecture. When loaded, the model creates driving and driven ports using Python's Protocol or abc.ABC, writes explicit port contracts with preconditions and postconditions, and produces adapter stubs that enforce boundary integrity between domain logic and external systems.
Protocol for structural subtyping when duck-typing is sufficient; use ABC only when nominal inheritance or abstract method enforcement is requiredv2 suffix when breaking changes are needed — never mutate an existing Protocol's method signaturesmypy --strict to verify that all adapters actually conform to the declared portsUse this skill when:
Avoid this skill for:
Protocol has negligible overhead in practice, but if profiling confirms otherwise, inline the callCatalog external dependencies — List every I/O boundary your system crosses: databases, HTTP clients, file systems, message queues, third-party APIs, email services, payment gateways. For each dependency, determine whether it is a driven port (the core domain needs something from infrastructure) or a driving port (external actors call into the system through an interface).
Checkpoint: Every external library listed in pyproject.toml under [project.dependencies] must map to exactly one port — either as a driven port implementation or as part of the adapter boundary.
Draft the Protocol or ABC — For each driven port, write the minimal interface using typing.Protocol (preferred for structural subtyping) or abc.ABC (when you need abstract method enforcement or inheritance chains). Name the protocol from the consumer's perspective: if OrderService needs to persist orders, call it OrderRepository, not DatabaseConnection.
Checkpoint: The Protocol must declare only the methods the core domain actually calls. If a concrete implementation has 20 methods but the consumer only uses 3, the Protocol should have exactly 3 — this is Interface Segregation in practice.
Write preconditions and postconditions — Add structured docstrings to every port method. Document what must be true before the call (preconditions), what the caller can expect after (postconditions), and any invariants that hold across calls.
Checkpoint: If a precondition cannot be expressed as a type hint, state it explicitly in the docstring. Example: "order must not be None — calling code is responsible for validation before invoking this method."
Implement concrete adapters — Create adapter classes in a separate package (e.g., adapters/) that import both the port Protocol and the specific infrastructure library. Each adapter implements exactly one port. Never let an adapter contain logic from two different ports — single responsibility applies to adapters too.
Checkpoint: Run the adapter through mypy against its declared Protocol with --strict. Any missing method or type mismatch must be resolved before merging.
Create in-memory stubs for testing — For each driven port, produce a FakePortName class that lives in the test package. Implement it with dictionaries, lists, or mock objects to provide deterministic behavior without touching real infrastructure.
Checkpoint: A unit test that imports a fake adapter and exercises the core domain should pass with zero network calls, zero database connections, and zero file system access.
Wire at composition root — In main.py or a dedicated bootstrap.py, import both the port Protocol and all concrete adapters. Construct each adapter and pass it to the core services that depend on it. The composition root is the only file that imports from both layers simultaneously.
Checkpoint: Run a startup smoke test that calls the production bootstrap function and exercises one complete use case end-to-end with real infrastructure.
Driving ports (also called secondary ports) expose use cases to external actors — controllers, CLI commands, scheduled jobs. They define the entry points into your domain. Use @runtime_checkable when you want isinstance checks at runtime; omit it for performance-critical paths where only static typing is needed.
from __future__ import annotations
from typing import Protocol, runtime_checkable
@runtime_checkable
class PlaceOrderCommandPort(Protocol):
"""Driving port: defines the interface external actors use to place orders."""
def execute(
self,
user_id: str,
product_id: str,
quantity: int,
) -> str:
"""Place a new order for the given product.
Preconditions:
- user_id must be a non-empty string matching UUID v4 format
- product_id must reference an active product in the catalog
- quantity must be a positive integer (>= 1)
Postconditions:
- Returns a non-empty order ID string on success
- Order state transitions to PENDING in the persistence layer
- Inventory reservation is created if stock is available
"""
...
@runtime_checkable
class CancelOrderCommandPort(Protocol):
"""Driving port: defines the interface for cancelling existing orders."""
def execute(self, order_id: str) -> bool:
"""Cancel an existing order by its ID.
Preconditions:
- order_id must belong to an order in PENDING or CONFIRMED state
- Order must not have already shipped
Postconditions:
- Returns True if cancellation succeeded, False if already fulfilled
- Reserved inventory is released back to available stock
"""
...
Driven ports (also called primary ports) define what the core domain needs from external systems. These are implemented by infrastructure adapters. Use abc.ABC when you want abstract method enforcement or inheritance hierarchies among adapters.
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Sequence
@dataclass(frozen=True)
class Product:
"""Domain entity representing a product in the catalog."""
id: str
name: str
sku: str
price_cents: int
in_stock: bool = True
class ProductRepository(ABC):
"""Driven port: defines what the domain needs from product persistence."""
@abstractmethod
def get_by_id(self, product_id: str) -> Product | None:
"""Retrieve a product by its unique identifier.
Precondition:
- product_id must be a non-empty string
Postcondition:
- Returns the Product if found, None if no product matches
"""
...
@abstractmethod
def get_available_products(self, limit: int = 50) -> Sequence[Product]:
"""Retrieve available products, ordered by creation date descending.
Precondition:
- limit must be between 1 and 1000 inclusive
Postcondition:
- Returns at most `limit` products where in_stock is True
"""
...
@abstractmethod
def save(self, product: Product) -> Product:
"""Persist a product to the catalog.
Precondition:
- product.id must be non-empty
- product.price_cents must be non-negative
Postcondition:
- Returns the persisted product (may have updated fields like updated_at)
- If a product with the same ID exists, this performs an upsert
"""
...
class StockCheckRepository(ABC):
"""Driven port: defines what the domain needs for inventory operations."""
@abstractmethod
def reserve_stock(self, product_id: str, quantity: int) -> bool:
"""Reserve stock for a pending order.
Precondition:
- product_id must exist in the catalog
- quantity must be >= 1
Postcondition:
- Returns True if reservation succeeded (stock was available)
- Returns False if insufficient stock is available
"""
...
@abstractmethod
def release_reservation(self, product_id: str, quantity: int) -> None:
"""Release a previously made stock reservation.
Postcondition:
- Stock count increases by the reserved quantity
"""
...
Choosing between Protocol and ABC is one of the most common port design decisions. Here's a concrete comparison showing when each is appropriate.
# ❌ BAD: Using ABC for every interface, even when structural subtyping would suffice.
# This forces inheritance hierarchies that limit flexibility and make testing harder.
from abc import ABC, abstractmethod
class BadDatabaseClient(ABC):
"""ABC forces subclassing — every new database requires a new class declaration."""
@abstractmethod
def query(self, sql: str, params: tuple) -> list[dict]: ...
@abstractmethod
def execute(self, sql: str, params: tuple) -> int: ...
@abstractmethod
def close(self) -> None: ...
class GoodDatabaseClient:
"""No inheritance required — just implement the methods. Structural subtyping."""
pass # Will satisfy any Protocol with these three methods
from typing import Protocol
class DatabaseClient(Protocol):
"""Protocol-based interface — any object with matching signatures satisfies it."""
def query(self, sql: str, params: tuple) -> list[dict]: ...
def execute(self, sql: str, params: tuple) -> int: ...
def close(self) -> None: ...
# ✅ GOOD: SQLAlchemy adapter implements DatabaseClient structurally — no inheritance.
import sqlalchemy as sa
class SQLAlchemyClient:
"""Satisfies DatabaseClient Protocol without inheriting from it."""
def __init__(self, engine: sa.Engine) -> None:
self.engine = engine
def query(self, sql: str, params: tuple) -> list[dict]:
with self.engine.connect() as conn:
result = conn.execute(sa.text(sql), params) # type: ignore[arg-type]
return [dict(row._mapping) for row in result.fetchall()]
def execute(self, sql: str, params: tuple) -> int:
with self.engine.connect() as conn:
result = conn.execute(sa.text(sql), params) # type: ignore[arg-type]
conn.commit()
return result.rowcount # type: ignore[attr-defined]
def close(self) -> None:
self.engine.dispose()
# ✅ GOOD: In-memory test double satisfies the same Protocol — no inheritance chain needed.
class MemoryClient:
"""Fake implementation for unit tests — satisfies DatabaseClient structurally."""
def __init__(self) -> None:
self._tables: dict[str, list[dict]] = {}
def query(self, sql: str, params: tuple) -> list[dict]:
return [] # Simplified for illustration
def execute(self, sql: str, params: tuple) -> int:
return 1 # Simplified for illustration
def close(self) -> None:
pass
Every port method should document its contract explicitly. This makes the interface self-documenting and enables static analysis tools to enforce constraints. Use attrs.define or dataclass(frozen=True) for immutable request/response types.
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Protocol
class TransferStatus(Enum):
PENDING = "pending"
COMPLETED = "completed"
FAILED = "failed"
REVERSED = "reversed"
@dataclass(frozen=True)
class TransferRequest:
"""Immutable request object for the transfer port contract."""
from_account_id: str
to_account_id: str
amount_cents: int
currency: str = "USD"
reference: str | None = None
def __post_init__(self) -> None:
if self.amount_cents <= 0:
raise ValueError("Transfer amount must be positive")
if self.from_account_id == self.to_account_id:
raise ValueError("Cannot transfer to the same account")
@dataclass(frozen=True)
class TransferResult:
"""Immutable result object returned by the transfer port."""
transfer_id: str
status: TransferStatus
from_account_id: str
to_account_id: str
amount_cents: int
error_message: str | None = None
class TransferPort(Protocol):
"""Port contract for initiating fund transfers.
Invariants (hold across all calls):
- transfer_id values are globally unique UUID v4 strings
- amount_cents is always stored as an integer (no floating point)
- Status transitions follow: PENDING -> COMPLETED | FAILED -> REVERSED
"""
def execute_transfer(self, request: TransferRequest) -> TransferResult:
"""Execute a fund transfer between two accounts.
Preconditions:
- from_account_id must exist and have sufficient balance
- to_account_id must exist and be an active account
- request.currency must match the accounts' currency settings
- request.amount_cents must not exceed the daily transfer limit (100,000 USD)
Postconditions:
- A TransferResult is returned with status COMPLETED on success
- On failure, returns TransferResult with FAILED status and error_message set
- Account balances are updated atomically within a database transaction
- An audit log entry is created for both success and failure cases
"""
...
def get_transfer_status(self, transfer_id: str) -> TransferResult | None:
"""Query the current status of a previously initiated transfer.
Precondition:
- transfer_id must be a valid UUID v4 string
Postcondition:
- Returns TransferResult with the latest known status
- Returns None if no transfer with this ID exists in the system
"""
...
# ✅ GOOD: Concrete adapter implementing the full port contract.
from uuid import UUID, uuid4
class BankingTransferAdapter:
"""Real-world adapter implementing TransferPort for an external banking API."""
def __init__(self, api_base_url: str, api_key: str) -> None:
self._base_url = api_base_url.rstrip("/")
self._api_key = api_key
def execute_transfer(self, request: TransferRequest) -> TransferResult:
# Real adapter would call external banking API here
transfer_id = str(uuid4())
try:
# Simulate API call — production code uses httpx or requests
response_status = TransferStatus.COMPLETED # Placeholder for real call
except ConnectionError as exc:
return TransferResult(
transfer_id=transfer_id,
status=TransferStatus.FAILED,
from_account_id=request.from_account_id,
to_account_id=request.to_account_id,
amount_cents=request.amount_cents,
error_message=f"Banking API unreachable: {exc}",
)
return TransferResult(
transfer_id=transfer_id,
status=response_status,
from_account_id=request.from_account_id,
to_account_id=request.to_account_id,
amount_cents=request.amount_cents,
)
def get_transfer_status(self, transfer_id: str) -> TransferResult | None:
# Real adapter would query the banking API for status
return None # Placeholder — real implementation queries API
Every driven port should have a fake implementation for unit testing. This isolates domain logic from infrastructure without needing unittest.mock.MagicMock — the fake is a first-class class that you can inspect, assert against, and reason about.
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Sequence
@dataclass
class FakeProductRepo:
"""In-memory stub for ProductRepository used exclusively in unit tests."""
_products: dict[str, Product] = field(default_factory=dict)
_call_log: list[str] = field(default_factory=list)
def get_by_id(self, product_id: str) -> Product | None:
self._call_log.append(f"get_by_id({product_id!r})")
return self._products.get(product_id)
def get_available_products(self, limit: int = 50) -> Sequence[Product]:
self._call_log.append(f"get_available_products(limit={limit})")
available = [p for p in self._products.values() if p.in_stock]
return sorted(available, key=lambda p: p.id)[:limit]
def save(self, product: Product) -> Product:
self._call_log.append(f"save(product_id={product.id!r})")
self._products[product.id] = product
return product
# ---- Inspection helpers for tests ----
def called_with(self, product_id: str) -> bool:
"""Check if get_by_id was called with the given ID."""
return f"get_by_id({product_id!r})" in self._call_log
def last_action(self) -> str | None:
"""Return the most recent operation performed on this fake."""
return self._call_log[-1] if self._call_log else None
def product_count(self) -> int:
"""Return number of products currently stored."""
return len(self._products)
def reset(self) -> None:
"""Clear all state — useful between test cases."""
self._products.clear()
self._call_log.clear()
@dataclass
class FakeStockCheckRepo:
"""In-memory stub for StockCheckRepository used exclusively in unit tests."""
_stock_levels: dict[str, int] = field(default_factory=dict)
_reservations: dict[str, int] = field(default_factory=dict)
_call_log: list[str] = field(default_factory=list)
def reserve_stock(self, product_id: str, quantity: int) -> bool:
self._call_log.append(f"reserve({product_id!r}, qty={quantity})")
available = self._stock_levels.get(product_id, 0) - self._reservations.get(product_id, 0)
if available >= quantity:
self._reservations[product_id] = self._reservations.get(product_id, 0) + quantity
return True
return False
def release_reservation(self, product_id: str, quantity: int) -> None:
self._call_log.append(f"release({product_id!r}, qty={quantity})")
current = self._reservations.get(product_id, 0)
self._reservations[product_id] = max(0, current - quantity)
# ---- Inspection helpers for tests ----
def reservation_count(self, product_id: str) -> int:
return self._reservations.get(product_id, 0)
def reset(self) -> None:
self._stock_levels.clear()
self._reservations.clear()
self._call_log.clear()
OrderRepository not DatabaseConnection, HttpClient not RequestsClienttyping.Protocol (structural subtyping) by default; reserve abc.ABC for interfaces requiring abstract method enforcement or inheritance hierarchiescore/ports.py or domain/ports.py) — never inside infrastructure packagesProductRepositoryV2) rather than mutating the existing onesqlalchemy, httpx, pymongo) into the Protocol definition file — ports must be 100% framework-agnosticisinstance(adapter, SomeProtocol) at runtime unless you've decorated the Protocol with @runtime_checkable and explicitly need that check — static type checking is preferred**kwargs or Any types in port method signatures — explicit typed parameters are the whole point of Protocol-based interfacesWhen implementing or reviewing port interfaces, produce:
| Skill | Purpose |
|---|---|
hexagonal-architecture | The overarching architecture pattern that uses ports and adapters; this skill defines how to declare the port interfaces themselves |
composition-root | Wires concrete adapters to their declared ports at application startup — the natural next step after defining ports |
test-driven-development | Write port contracts first, then implement adapters against them in a TDD workflow |
anti-corruption-layer | Builds adapter boundaries that translate between external domain models and your internal domain model through ports |
dependency-inversion-principle | The SOLID principle that ports enforce: high-level modules depend on abstractions, not concretions |