ADR-001: Use Pydantic for Domain Models#

Status#

Accepted

Context#

The project follows hexagonal architecture. A core tenet of hexagonal architecture is that the domain layer should be free of framework dependencies so that it remains testable and portable.

The architect initially proposed frozen dataclasses for all domain entities (FeedbackDocument, AnalysisRequest, AnalysisResult, LLMResponse, TenantApiKey) to keep the domain layer framework-free. Pydantic would only be used at the API boundary in api/schemas/.

This would require a translation layer: every incoming request must be converted from a Pydantic API schema to a domain dataclass, and every outgoing result converted back. This doubles the number of data definitions and adds boilerplate mapping code.

Decision#

Use Pydantic BaseModel with model_config = ConfigDict(frozen=True) for all domain entities.

Options Considered#

Option A: Frozen dataclasses in domain (rejected)#

  • Pro: Domain has zero framework dependencies. Fully portable.

  • Con: Requires a parallel set of Pydantic schemas at the API boundary. Every field is defined twice. A translation layer must be written, tested, and maintained. For a v1 with no domain logic (entities are pure DTOs), this is overhead without payoff.

Option B: Pydantic everywhere (chosen)#

  • Pro: One set of data definitions. Validation, serialization, JSON Schema generation, and immutability all come free. No translation boilerplate.

  • Con: Domain layer depends on Pydantic. If Pydantic introduces a breaking change, the domain is affected.

  • Mitigation: Pydantic v2 has a stable API. The coupling is to a data validation library, not to a web framework. The domain does not use FastAPI, uvicorn, or HTTP concepts — only BaseModel and Field.

Option C: attrs or plain classes (not considered)#

Would introduce a third-party library for a role Pydantic already fills.

Consequences#

  • Domain entities are Pydantic models. They can be used directly in tests without any factory or conversion.

  • API schemas (api/schemas.py) remain separate from domain models — they define the public API contract and may diverge from internal models over time (see ADR-007).

  • If a future requirement demands a framework-free domain (e.g., extracting the domain into a shared library), the migration path is: replace BaseModel with dataclasses and add the translation layer at that point.

Participants#

  • Architect (proposed dataclasses)

  • Devil’s advocate (proposed Pydantic, accepted)

  • Domain expert (supported Pydantic for pragmatic reasons)