Skip to content

Adopt Domain Driven Design

Context and Problem Statement

The HDC Auth Proxy protects heterogeneous resources (S3 buckets, HTTP endpoints, applications) for both human users and M2M services, using multiple authentication methods (OAuth2/OIDC, API keys, JWT) and multiple identity providers (AWS Cognito, federated CIAM). How should we structure the core logic to manage this complexity while keeping the codebase maintainable and testable?

Decision Drivers

  • Multiple authentication methods and identity providers create complex interaction paths
  • The proxy must remain stateless for M2M and session-based for browser users — two fundamentally different flows
  • Business rules (who can access what) must be independent of transport (HTTP) and infrastructure (AWS SDK, JWT libraries)
  • The team follows hexagonal architecture principles with Postel's Law for typing

Considered Options

  • Flat module structure with framework-coupled logic
  • Layered architecture without explicit bounded contexts
  • Domain Driven Design with bounded contexts and hexagonal architecture

Decision Outcome

Chosen option: "Domain Driven Design with bounded contexts and hexagonal architecture", because it isolates business rules from infrastructure, makes each concern independently testable, and maps naturally to the four distinct responsibilities of the proxy.

Bounded Contexts

Context Purpose Key question
Identity WHO is making the request Human user or M2M service? From which IdP?
Authentication HOW identity was proven Valid JWT? Correct API key? OAuth2 token?
Authorization WHAT they are allowed to do Do they have permissions for this resource?
Resource WHAT is being PROTECTED S3 bucket? HTTP endpoint? Application?

Session is NOT a domain bounded context — it is an infrastructure concern that lives in adapters/.

Domain modeling conventions

  • All domain models are frozen dataclasses with slots — no external dependencies
  • StrEnum for JSON-friendly enumerations
  • Immutable collections (frozenset, tuple) for frozen dataclass compatibility
  • Result-type pattern (AuthenticationResult, AccessResult) instead of exceptions for flow control
  • Domain errors as dataclasses, not Python exceptions — exceptions are reserved for unexpected infrastructure failures
  • Validation in __post_init__ only for domain invariants

Consequences

  • Good, because domain logic is pure Python with zero dependencies — fast, portable, easy to test
  • Good, because each bounded context can evolve independently
  • Good, because adapters (Pydantic, JWT libs, AWS SDK) can be swapped without touching domain code
  • Neutral, because it requires discipline to maintain boundary separation as the codebase grows
  • Bad, because mapping between adapter DTOs and domain dataclasses adds boilerplate

Confirmation

  • The domain/ package must have zero imports from external libraries — verified by linting rules
  • Type checking with ty and linting with ruff enforce structural correctness
  • Code review ensures new types are placed in the correct bounded context

More Information

See Domain Layer for the full model reference and implementation order.