Result-type dataclasses to avoid isinstance
Context and Problem Statement
Output ports (e.g., TokenValidator, ApiKeyValidator) need to signal both success and failure. How should they communicate results to service-layer callers without introducing isinstance checks in business logic?
Decision Drivers
- No
isinstancein business logic (project guideline, anti-pattern #2) - Consistent with existing
AuthenticationResultandAccessResultpatterns in the domain - Services should branch on simple boolean flags, not on type discriminators
- Failures are expected outcomes (invalid token, unknown key), not exceptional situations
Considered Options
- Union return types (
TokenClaims | AuthenticationError) requiringisinstancedispatch - Raising exceptions for expected failures
- Result-type dataclasses with a
valid: booldiscriminator and optional fields
Decision Outcome
Chosen option: "Result-type dataclasses", because they eliminate isinstance in services and treat expected failures as first-class data.
The pattern
Each output port returns a frozen dataclass with:
- valid: bool — the discriminator
- Success fields (populated when valid=True, enforced by __post_init__)
- failure_reason: str | None (populated when valid=False, enforced by __post_init__)
@dataclass(frozen=True, slots=True)
class TokenValidationResult:
valid: bool
claims: TokenClaims | None = None
failure_reason: str | None = None
def __post_init__(self) -> None:
if self.valid and self.claims is None:
raise ValueError("Valid token must produce TokenClaims")
if not self.valid and self.failure_reason is None:
raise ValueError("Invalid token must include a failure_reason")
Services branch on result.valid:
if not validation.valid:
return AuthenticationResult(..., failure_reason=validation.failure_reason)
# Safe to access validation.claims here
Current result types
| Type | Port | Success field | Used by |
|---|---|---|---|
TokenValidationResult |
TokenValidator |
claims: TokenClaims |
TokenMethodAuthenticator |
KeyValidationResult |
ApiKeyValidator |
principal: Principal |
ApiKeyMethodAuthenticator |
Consequences
- Good, because no
isinstancein service-layer business logic - Good, because
__post_init__invariants make illegal states unrepresentable - Good, because consistent with domain's existing
AuthenticationResultandAccessResult - Good, because failure reasons are structured data, not exception messages
- Neutral, because result types add classes to the domain layer
- Bad, because callers must remember to check
validbefore accessing success fields
Confirmation
- Domain invariant tests in
test_domain.pyverify__post_init__enforcement - Service tests in
test_authentication_service.pyverify correct branching onvalid - Architecture tests verify result types live in
domain/, not in services
More Information
- Related: ADR-0004 Strategy pattern
- Implementation:
src/hdc_auth_proxy/domain/authentication.py