Sync-first Protocols
Context and Problem Statement
Ports define the contract between services and adapters. Should port Protocols use synchronous or asynchronous method signatures? Adapters will eventually perform I/O (HTTP calls to IdPs, database queries for policies), which naturally suits async.
Decision Drivers
- Domain and services layers are pure logic with no I/O — async adds no value there
- Adapter layer (not yet built) will introduce I/O to external systems
- Premature async infects the entire call chain (
async/awaitis viral) - Testing sync code is simpler — no event loop, no async fixtures needed
- The proxy will likely run on an async framework (e.g., FastAPI), but services should remain framework-agnostic
Considered Options
- Async from the start (all Protocols use
async def) - Sync now, parallel async Protocols later (e.g.,
AsyncTokenValidatoralongsideTokenValidator) - Sync only, convert to async when needed
Decision Outcome
Chosen option: "Sync now, parallel async Protocols later", because it avoids premature complexity while providing a clear migration path.
Current state
All port Protocols are synchronous:
class TokenValidator(Protocol):
def validate(self, token: str) -> TokenValidationResult: ...
class PolicyRepository(Protocol):
def rules_for_resource(self, resource_uri: str) -> tuple[PolicyRule, ...]: ...
Migration path
When the adapter layer introduces I/O:
- Add async variants alongside sync Protocols (e.g.,
AsyncTokenValidator) - Async adapters implement the async Protocol
- Services that need async get async counterparts or are wrapped with
asyncio.to_thread - Sync Protocols remain for testing and non-I/O adapters (in-memory stubs)
Consequences
- Good, because current tests are simple and fast (no async fixtures)
- Good, because domain and services layers stay framework-agnostic
- Good, because sync stubs are trivial to write for TDD
- Neutral, because async migration will require adding parallel Protocol definitions
- Bad, because the eventual async migration touches multiple files
Confirmation
- All current Protocols use
def, notasync def - Test suite runs without
pytest-asyncioor event loop setup - Architecture tests verify services have no I/O dependencies
More Information
- Related: ADR-0002 Adopt DDD
- Affected files:
src/hdc_auth_proxy/ports/authentication.py,src/hdc_auth_proxy/ports/authorization.py,src/hdc_auth_proxy/ports/input.py