Skip to content

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 isinstance in business logic (project guideline, anti-pattern #2)
  • Consistent with existing AuthenticationResult and AccessResult patterns 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) requiring isinstance dispatch
  • Raising exceptions for expected failures
  • Result-type dataclasses with a valid: bool discriminator 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 isinstance in service-layer business logic
  • Good, because __post_init__ invariants make illegal states unrepresentable
  • Good, because consistent with domain's existing AuthenticationResult and AccessResult
  • 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 valid before accessing success fields

Confirmation

  • Domain invariant tests in test_domain.py verify __post_init__ enforcement
  • Service tests in test_authentication_service.py verify correct branching on valid
  • Architecture tests verify result types live in domain/, not in services

More Information