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
tyand linting withruffenforce 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.