Skip to content

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 / await is 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., AsyncTokenValidator alongside TokenValidator)
  • 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:

  1. Add async variants alongside sync Protocols (e.g., AsyncTokenValidator)
  2. Async adapters implement the async Protocol
  3. Services that need async get async counterparts or are wrapped with asyncio.to_thread
  4. 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, not async def
  • Test suite runs without pytest-asyncio or 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