Project 10 (Final Project): Production Validation Framework

Project 10 (Final Project): Production Validation Framework

Build a complete production application demonstrating all Pydantic featuresโ€”API validation, settings management, database models, LLM integration, custom types, and comprehensive error handling. This capstone project proves mastery of data validation architecture in Python.


Learning Objectives

By completing this project, you will:

  1. Design layered validation architectures - Understand how validation flows through API, service, and repository layers
  2. Create reusable custom type libraries - Build domain-specific types that enforce business rules consistently
  3. Implement comprehensive error handling - Aggregate and present errors in user-friendly formats across all layers
  4. Optimize validation performance - Use model_construct, caching, and lazy validation for production workloads
  5. Integrate all Pydantic features - Combine settings, validators, generics, discriminated unions, and ORM integration
  6. Build production-ready systems - Apply testing pyramids, deployment patterns, and observability for validated data

Deep Theoretical Foundation

Layered Architecture: API to Service to Repository

Production applications require clear separation of concerns. Each layer has distinct validation responsibilities:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                            CLIENT / EXTERNAL WORLD                          โ”‚
โ”‚                     (Mobile apps, web browsers, other APIs)                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                      โ”‚
                                      โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                              API LAYER                                      โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚  Responsibilities:                                                   โ”‚   โ”‚
โ”‚  โ”‚  - Parse HTTP requests (JSON, form data, query params)              โ”‚   โ”‚
โ”‚  โ”‚  - Validate INPUT MODELS (UserCreate, OrderRequest)                 โ”‚   โ”‚
โ”‚  โ”‚  - Handle authentication/authorization                               โ”‚   โ”‚
โ”‚  โ”‚  - Transform domain objects to OUTPUT MODELS (UserResponse)         โ”‚   โ”‚
โ”‚  โ”‚  - Return structured error responses                                 โ”‚   โ”‚
โ”‚  โ”‚                                                                      โ”‚   โ”‚
โ”‚  โ”‚  Pydantic Features:                                                  โ”‚   โ”‚
โ”‚  โ”‚  - BaseModel for request/response schemas                           โ”‚   โ”‚
โ”‚  โ”‚  - Field constraints (min_length, ge, le)                           โ”‚   โ”‚
โ”‚  โ”‚  - Custom validators for input normalization                        โ”‚   โ”‚
โ”‚  โ”‚  - response_model for serialization                                 โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                      โ”‚
                                      โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                            SERVICE LAYER                                    โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚  Responsibilities:                                                   โ”‚   โ”‚
โ”‚  โ”‚  - Implement business logic and rules                                โ”‚   โ”‚
โ”‚  โ”‚  - Orchestrate between repositories                                  โ”‚   โ”‚
โ”‚  โ”‚  - Validate DOMAIN MODELS (User, Order with business rules)         โ”‚   โ”‚
โ”‚  โ”‚  - Handle cross-entity validation                                    โ”‚   โ”‚
โ”‚  โ”‚  - Emit domain events                                                โ”‚   โ”‚
โ”‚  โ”‚                                                                      โ”‚   โ”‚
โ”‚  โ”‚  Pydantic Features:                                                  โ”‚   โ”‚
โ”‚  โ”‚  - @model_validator for cross-field validation                      โ”‚   โ”‚
โ”‚  โ”‚  - Custom types for domain concepts (Money, PhoneNumber)            โ”‚   โ”‚
โ”‚  โ”‚  - Discriminated unions for polymorphic domain objects              โ”‚   โ”‚
โ”‚  โ”‚  - Result types for operation outcomes                               โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                      โ”‚
                                      โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                          REPOSITORY LAYER                                   โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚  Responsibilities:                                                   โ”‚   โ”‚
โ”‚  โ”‚  - Persist and retrieve data                                         โ”‚   โ”‚
โ”‚  โ”‚  - Translate between domain and DB models                           โ”‚   โ”‚
โ”‚  โ”‚  - Handle database constraints                                       โ”‚   โ”‚
โ”‚  โ”‚  - Manage transactions                                               โ”‚   โ”‚
โ”‚  โ”‚                                                                      โ”‚   โ”‚
โ”‚  โ”‚  Pydantic Features:                                                  โ”‚   โ”‚
โ”‚  โ”‚  - SQLModel for unified Pydantic + SQLAlchemy models               โ”‚   โ”‚
โ”‚  โ”‚  - from_attributes for ORM object conversion                        โ”‚   โ”‚
โ”‚  โ”‚  - model_construct for trusted internal data (skip validation)      โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                      โ”‚
                                      โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                        DATABASE / EXTERNAL SERVICES                         โ”‚
โ”‚                     (PostgreSQL, Redis, LLM APIs, etc.)                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Key Principle: Validate at boundaries. Data crossing a boundary (external to internal, layer to layer) must be validated at that point.

Model Layering: Input to Domain to Output

A single concept (e.g., โ€œUserโ€) requires multiple models at different layers:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                           MODEL HIERARCHY                                    โ”‚
โ”‚                                                                             โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”        โ”‚
โ”‚   โ”‚   INPUT MODEL   โ”‚    โ”‚  DOMAIN MODEL   โ”‚    โ”‚  OUTPUT MODEL   โ”‚        โ”‚
โ”‚   โ”‚  (API Request)  โ”‚    โ”‚ (Business Core) โ”‚    โ”‚ (API Response)  โ”‚        โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜        โ”‚
โ”‚            โ”‚                      โ”‚                      โ”‚                  โ”‚
โ”‚   UserCreate               User (DB)              UserResponse              โ”‚
โ”‚   โ”œโ”€โ”€ email: EmailStr      โ”œโ”€โ”€ id: int            โ”œโ”€โ”€ id: int              โ”‚
โ”‚   โ”œโ”€โ”€ name: str            โ”œโ”€โ”€ email: str         โ”œโ”€โ”€ email: str           โ”‚
โ”‚   โ”œโ”€โ”€ password: str        โ”œโ”€โ”€ name: str          โ”œโ”€โ”€ name: str            โ”‚
โ”‚   โ””โ”€โ”€ (required fields)    โ”œโ”€โ”€ hashed_password    โ”œโ”€โ”€ created_at           โ”‚
โ”‚                            โ”œโ”€โ”€ created_at         โ”œโ”€โ”€ task_count           โ”‚
โ”‚                            โ””โ”€โ”€ (relationships)    โ””โ”€โ”€ (computed fields)    โ”‚
โ”‚                                                                             โ”‚
โ”‚   UserUpdate                                       UserListResponse         โ”‚
โ”‚   โ”œโ”€โ”€ email: EmailStr?                             โ”œโ”€โ”€ items: list[User]   โ”‚
โ”‚   โ”œโ”€โ”€ name: str?                                   โ”œโ”€โ”€ total: int          โ”‚
โ”‚   โ””โ”€โ”€ (all optional)                               โ”œโ”€โ”€ page: int           โ”‚
โ”‚                                                    โ””โ”€โ”€ per_page: int       โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Model Flow:

# 1. API receives input model
@app.post("/users", response_model=UserResponse)
async def create_user(user: UserCreate):  # Validated input
    # 2. Service layer creates domain model
    domain_user = user_service.create(user)
    # 3. Response model serializes output
    return domain_user  # Automatically converted to UserResponse

Custom Type Libraries for Domains

Reusable custom types enforce consistency across your entire application:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                        DOMAIN TYPE LIBRARY                                  โ”‚
โ”‚                                                                             โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚   โ”‚  types/money.py                                                   โ”‚    โ”‚
โ”‚   โ”‚                                                                    โ”‚    โ”‚
โ”‚   โ”‚  class Money:                                                     โ”‚    โ”‚
โ”‚   โ”‚      amount: Decimal (always 2 decimal places)                    โ”‚    โ”‚
โ”‚   โ”‚      currency: str (ISO 4217 code)                                โ”‚    โ”‚
โ”‚   โ”‚                                                                    โ”‚    โ”‚
โ”‚   โ”‚      - Arithmetic operations (+, -, *)                            โ”‚    โ”‚
โ”‚   โ”‚      - Currency conversion                                         โ”‚    โ”‚
โ”‚   โ”‚      - Comparison operators                                        โ”‚    โ”‚
โ”‚   โ”‚      - JSON serialization: {"amount": "99.99", "currency": "USD"} โ”‚    โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                                             โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚   โ”‚  types/phone.py                                                   โ”‚    โ”‚
โ”‚   โ”‚                                                                    โ”‚    โ”‚
โ”‚   โ”‚  PhoneNumber = Annotated[str, BeforeValidator(normalize_phone)]   โ”‚    โ”‚
โ”‚   โ”‚                                                                    โ”‚    โ”‚
โ”‚   โ”‚  Input: "(555) 123-4567", "5551234567", "+1-555-123-4567"        โ”‚    โ”‚
โ”‚   โ”‚  Output: "+15551234567" (E.164 format)                            โ”‚    โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                                             โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚   โ”‚  types/address.py                                                 โ”‚    โ”‚
โ”‚   โ”‚                                                                    โ”‚    โ”‚
โ”‚   โ”‚  class Address(BaseModel):                                        โ”‚    โ”‚
โ”‚   โ”‚      street: str                                                  โ”‚    โ”‚
โ”‚   โ”‚      city: str                                                    โ”‚    โ”‚
โ”‚   โ”‚      state: USStateCode                                           โ”‚    โ”‚
โ”‚   โ”‚      postal_code: PostalCode                                      โ”‚    โ”‚
โ”‚   โ”‚      country: CountryCode = "US"                                  โ”‚    โ”‚
โ”‚   โ”‚                                                                    โ”‚    โ”‚
โ”‚   โ”‚      @model_validator: Validate postal code matches state        โ”‚    โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                                             โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚   โ”‚  types/identifiers.py                                             โ”‚    โ”‚
โ”‚   โ”‚                                                                    โ”‚    โ”‚
โ”‚   โ”‚  UserId = Annotated[int, Field(ge=1)]                             โ”‚    โ”‚
โ”‚   โ”‚  OrderId = Annotated[str, Field(pattern=r'^ORD-[A-Z0-9]{10}$')]  โ”‚    โ”‚
โ”‚   โ”‚  SKU = Annotated[str, Field(pattern=r'^[A-Z]{3}-\d{6}$')]        โ”‚    โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Benefits of Custom Type Libraries:

  1. Single source of truth - Phone validation logic in one place
  2. Self-documenting - Types describe domain concepts
  3. Type safety - IDE understands Money is not just float
  4. Testable - Unit test types independently of models

Comprehensive Error Handling Strategy

Production applications need errors that help developers and users:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                      ERROR HANDLING ARCHITECTURE                            โ”‚
โ”‚                                                                             โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚   โ”‚  Layer 1: Pydantic ValidationError                                  โ”‚  โ”‚
โ”‚   โ”‚                                                                      โ”‚  โ”‚
โ”‚   โ”‚  - Field-level errors with location (path)                         โ”‚  โ”‚
โ”‚   โ”‚  - Error type codes (string_too_short, missing, etc.)              โ”‚  โ”‚
โ”‚   โ”‚  - Input value that failed                                          โ”‚  โ”‚
โ”‚   โ”‚  - Context for constraint violations                                โ”‚  โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚  โ”‚
โ”‚                               โ”‚                                           โ”‚  โ”‚
โ”‚                               โ–ผ                                           โ”‚  โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚   โ”‚  Layer 2: Application Error Aggregation                             โ”‚  โ”‚
โ”‚   โ”‚                                                                      โ”‚  โ”‚
โ”‚   โ”‚  class AppError:                                                    โ”‚  โ”‚
โ”‚   โ”‚      code: str           # "USER_EMAIL_TAKEN"                       โ”‚  โ”‚
โ”‚   โ”‚      message: str        # "Email already registered"               โ”‚  โ”‚
โ”‚   โ”‚      field: str?         # "email"                                  โ”‚  โ”‚
โ”‚   โ”‚      details: dict?      # {"existing_user_id": 123}               โ”‚  โ”‚
โ”‚   โ”‚                                                                      โ”‚  โ”‚
โ”‚   โ”‚  class ValidationResult:                                            โ”‚  โ”‚
โ”‚   โ”‚      success: bool                                                  โ”‚  โ”‚
โ”‚   โ”‚      data: T?                                                       โ”‚  โ”‚
โ”‚   โ”‚      errors: list[AppError]                                        โ”‚  โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                               โ”‚                                           โ”‚  โ”‚
โ”‚                               โ–ผ                                           โ”‚  โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚   โ”‚  Layer 3: HTTP Response Formatting                                  โ”‚  โ”‚
โ”‚   โ”‚                                                                      โ”‚  โ”‚
โ”‚   โ”‚  {                                                                  โ”‚  โ”‚
โ”‚   โ”‚    "success": false,                                                โ”‚  โ”‚
โ”‚   โ”‚    "error": {                                                       โ”‚  โ”‚
โ”‚   โ”‚      "code": "VALIDATION_ERROR",                                    โ”‚  โ”‚
โ”‚   โ”‚      "message": "Request validation failed",                        โ”‚  โ”‚
โ”‚   โ”‚      "errors": [                                                    โ”‚  โ”‚
โ”‚   โ”‚        {                                                            โ”‚  โ”‚
โ”‚   โ”‚          "field": "email",                                          โ”‚  โ”‚
โ”‚   โ”‚          "code": "USER_EMAIL_TAKEN",                                โ”‚  โ”‚
โ”‚   โ”‚          "message": "Email already registered"                      โ”‚  โ”‚
โ”‚   โ”‚        },                                                           โ”‚  โ”‚
โ”‚   โ”‚        {                                                            โ”‚  โ”‚
โ”‚   โ”‚          "field": "order.items[0].quantity",                        โ”‚  โ”‚
โ”‚   โ”‚          "code": "VALUE_TOO_LOW",                                   โ”‚  โ”‚
โ”‚   โ”‚          "message": "Quantity must be at least 1"                   โ”‚  โ”‚
โ”‚   โ”‚        }                                                            โ”‚  โ”‚
โ”‚   โ”‚      ]                                                              โ”‚  โ”‚
โ”‚   โ”‚    },                                                               โ”‚  โ”‚
โ”‚   โ”‚    "request_id": "req_abc123"                                       โ”‚  โ”‚
โ”‚   โ”‚  }                                                                  โ”‚  โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Error Handling Pattern:

from pydantic import ValidationError

class AppError(BaseModel):
    code: str
    message: str
    field: str | None = None
    details: dict | None = None

class ErrorResponse(BaseModel):
    success: bool = False
    error: dict
    request_id: str

def pydantic_to_app_errors(exc: ValidationError) -> list[AppError]:
    """Convert Pydantic errors to application errors."""
    errors = []
    for error in exc.errors():
        field_path = ".".join(str(loc) for loc in error["loc"])
        errors.append(AppError(
            code=error["type"].upper().replace("_", "_"),
            message=error["msg"],
            field=field_path,
            details={"input": error.get("input"), "ctx": error.get("ctx")}
        ))
    return errors

Performance Optimization Techniques

Validation has overhead. Production systems need optimization strategies:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                      PERFORMANCE OPTIMIZATION                               โ”‚
โ”‚                                                                             โ”‚
โ”‚   1. model_construct() - Skip Validation for Trusted Data                  โ”‚
โ”‚   โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚
โ”‚                                                                             โ”‚
โ”‚   # SLOW: Full validation (from database)                                  โ”‚
โ”‚   user = User.model_validate(db_row)                                       โ”‚
โ”‚                                                                             โ”‚
โ”‚   # FAST: Skip validation (data already validated when inserted)           โ”‚
โ”‚   user = User.model_construct(**db_row)                                    โ”‚
โ”‚                                                                             โ”‚
โ”‚   When to use model_construct:                                             โ”‚
โ”‚   - Loading from database (already validated on insert)                    โ”‚
โ”‚   - Internal service-to-service communication                              โ”‚
โ”‚   - Cached data retrieval                                                   โ”‚
โ”‚   - NEVER with external input!                                              โ”‚
โ”‚                                                                             โ”‚
โ”‚   2. Lazy Validation with __get_pydantic_core_schema__                     โ”‚
โ”‚   โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚
โ”‚                                                                             โ”‚
โ”‚   # Expensive validation (e.g., database lookup) only when needed          โ”‚
โ”‚   class LazyUser:                                                          โ”‚
โ”‚       def __get_pydantic_core_schema__(cls, ...):                          โ”‚
โ”‚           # Defer expensive checks until actually validated                โ”‚
โ”‚           ...                                                               โ”‚
โ”‚                                                                             โ”‚
โ”‚   3. Caching Validated Objects                                              โ”‚
โ”‚   โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚
โ”‚                                                                             โ”‚
โ”‚   from functools import lru_cache                                          โ”‚
โ”‚                                                                             โ”‚
โ”‚   @lru_cache(maxsize=1000)                                                 โ”‚
โ”‚   def get_validated_config(config_key: str) -> AppSettings:                โ”‚
โ”‚       # Validate once, cache result                                        โ”‚
โ”‚       return AppSettings.model_validate(load_config(config_key))           โ”‚
โ”‚                                                                             โ”‚
โ”‚   4. model_validate_json() vs model_validate()                              โ”‚
โ”‚   โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚
โ”‚                                                                             โ”‚
โ”‚   # SLOWER: Parse JSON to dict, then validate                              โ”‚
โ”‚   data = json.loads(json_string)                                           โ”‚
โ”‚   user = User.model_validate(data)                                         โ”‚
โ”‚                                                                             โ”‚
โ”‚   # FASTER: Direct JSON parsing in Rust core                               โ”‚
โ”‚   user = User.model_validate_json(json_string)                             โ”‚
โ”‚                                                                             โ”‚
โ”‚   5. Selective Validation with computed_field                               โ”‚
โ”‚   โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚
โ”‚                                                                             โ”‚
โ”‚   class Order(BaseModel):                                                  โ”‚
โ”‚       items: list[OrderItem]                                               โ”‚
โ”‚                                                                             โ”‚
โ”‚       @computed_field                                                      โ”‚
โ”‚       @cached_property                                                     โ”‚
โ”‚       def total(self) -> Money:                                            โ”‚
โ”‚           # Computed only when accessed                                    โ”‚
โ”‚           return sum(item.subtotal for item in self.items)                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Testing Pyramid for Validation

A comprehensive testing strategy ensures validation correctness:

                           โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                          โ•ฑ             โ•ฒ
                         โ•ฑ   E2E Tests   โ•ฒ
                        โ•ฑ   (Few, Slow)   โ•ฒ
                       โ•ฑ                   โ•ฒ
                      โ•ฑ  - Full API flows   โ•ฒ
                     โ•ฑ  - Real database      โ•ฒ
                    โ•ฑ  - External services    โ•ฒ
                   โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
                  โ•ฑ     Integration Tests       โ•ฒ
                 โ•ฑ       (Medium Speed)          โ•ฒ
                โ•ฑ                                 โ•ฒ
               โ•ฑ  - API endpoint validation        โ•ฒ
              โ•ฑ  - Service layer with mocked repos  โ•ฒ
             โ•ฑ  - Database constraint testing        โ•ฒ
            โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
           โ•ฑ            Unit Tests                     โ•ฒ
          โ•ฑ           (Many, Fast)                      โ•ฒ
         โ•ฑ                                               โ•ฒ
        โ•ฑ  - Individual model validation                  โ•ฒ
       โ•ฑ  - Custom type behavior                           โ•ฒ
      โ•ฑ  - Validator functions                              โ•ฒ
     โ•ฑ  - Error message formatting                           โ•ฒ
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Unit Test Examples:
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def test_money_type_parses_string():
    assert Money("99.99", "USD").amount == Decimal("99.99")

def test_phone_normalizes_formats():
    assert normalize_phone("(555) 123-4567") == "+15551234567"

def test_user_create_requires_password():
    with pytest.raises(ValidationError):
        UserCreate(email="test@example.com", name="Test")

Integration Test Examples:
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def test_create_user_endpoint_validates_email():
    response = client.post("/users", json={"email": "invalid"})
    assert response.status_code == 422
    assert "email" in response.json()["errors"][0]["field"]

def test_order_service_validates_inventory():
    with pytest.raises(InsufficientStockError):
        order_service.create_order(items=[{"sku": "ABC", "qty": 1000}])

E2E Test Examples:
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def test_complete_checkout_flow():
    # Create user
    user = api.create_user(...)
    # Add items to cart
    cart = api.add_to_cart(user.id, items=[...])
    # Checkout with payment
    order = api.checkout(cart.id, payment_method="card")
    # Verify order in database
    assert db.get_order(order.id).status == "confirmed"

Production Deployment Considerations

Validation in production requires additional infrastructure:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                     PRODUCTION INFRASTRUCTURE                               โ”‚
โ”‚                                                                             โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚   โ”‚  Configuration & Secrets                                            โ”‚  โ”‚
โ”‚   โ”‚                                                                      โ”‚  โ”‚
โ”‚   โ”‚  class Settings(BaseSettings):                                      โ”‚  โ”‚
โ”‚   โ”‚      # Core settings                                                 โ”‚  โ”‚
โ”‚   โ”‚      debug: bool = False                                            โ”‚  โ”‚
โ”‚   โ”‚      environment: Literal["dev", "staging", "prod"]                 โ”‚  โ”‚
โ”‚   โ”‚                                                                      โ”‚  โ”‚
โ”‚   โ”‚      # Database                                                      โ”‚  โ”‚
โ”‚   โ”‚      database_url: PostgresDsn                                      โ”‚  โ”‚
โ”‚   โ”‚      database_pool_size: int = Field(ge=1, le=100)                 โ”‚  โ”‚
โ”‚   โ”‚                                                                      โ”‚  โ”‚
โ”‚   โ”‚      # Secrets                                                       โ”‚  โ”‚
โ”‚   โ”‚      secret_key: SecretStr                                          โ”‚  โ”‚
โ”‚   โ”‚      api_key: SecretStr                                             โ”‚  โ”‚
โ”‚   โ”‚      llm_api_key: SecretStr                                         โ”‚  โ”‚
โ”‚   โ”‚                                                                      โ”‚  โ”‚
โ”‚   โ”‚      model_config = SettingsConfigDict(                             โ”‚  โ”‚
โ”‚   โ”‚          env_file=".env",                                           โ”‚  โ”‚
โ”‚   โ”‚          env_prefix="APP_",                                         โ”‚  โ”‚
โ”‚   โ”‚      )                                                               โ”‚  โ”‚
โ”‚   โ”‚                                                                      โ”‚  โ”‚
โ”‚   โ”‚      @model_validator(mode='after')                                 โ”‚  โ”‚
โ”‚   โ”‚      def validate_production_settings(self):                        โ”‚  โ”‚
โ”‚   โ”‚          if self.environment == "prod" and self.debug:             โ”‚  โ”‚
โ”‚   โ”‚              raise ValueError("Debug mode not allowed in prod")    โ”‚  โ”‚
โ”‚   โ”‚          return self                                                 โ”‚  โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                                                                             โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚   โ”‚  Observability                                                       โ”‚  โ”‚
โ”‚   โ”‚                                                                      โ”‚  โ”‚
โ”‚   โ”‚  - Validation error metrics (count by error type)                  โ”‚  โ”‚
โ”‚   โ”‚  - Validation latency histograms                                    โ”‚  โ”‚
โ”‚   โ”‚  - Structured logging with field paths                              โ”‚  โ”‚
โ”‚   โ”‚  - Error sampling for debugging                                     โ”‚  โ”‚
โ”‚   โ”‚                                                                      โ”‚  โ”‚
โ”‚   โ”‚  logger.info("validation_complete",                                 โ”‚  โ”‚
โ”‚   โ”‚      model="UserCreate",                                            โ”‚  โ”‚
โ”‚   โ”‚      duration_ms=12.5,                                              โ”‚  โ”‚
โ”‚   โ”‚      valid=True,                                                    โ”‚  โ”‚
โ”‚   โ”‚      request_id="req_abc123"                                        โ”‚  โ”‚
โ”‚   โ”‚  )                                                                   โ”‚  โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                                                                             โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚   โ”‚  Health Checks                                                       โ”‚  โ”‚
โ”‚   โ”‚                                                                      โ”‚  โ”‚
โ”‚   โ”‚  @app.get("/health")                                                โ”‚  โ”‚
โ”‚   โ”‚  async def health():                                                โ”‚  โ”‚
โ”‚   โ”‚      return {                                                        โ”‚  โ”‚
โ”‚   โ”‚          "status": "healthy",                                       โ”‚  โ”‚
โ”‚   โ”‚          "version": settings.version,                               โ”‚  โ”‚
โ”‚   โ”‚          "environment": settings.environment,                       โ”‚  โ”‚
โ”‚   โ”‚          "database": await check_db_connection(),                   โ”‚  โ”‚
โ”‚   โ”‚          "cache": await check_redis_connection(),                   โ”‚  โ”‚
โ”‚   โ”‚      }                                                               โ”‚  โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Project Specification

Functional Requirements

Build a production-ready e-commerce order management system that demonstrates mastery of all Pydantic features:

Core Features:

  1. User Management - Registration, authentication, profile management
  2. Product Catalog - Products with variants, pricing, inventory
  3. Order Processing - Cart, checkout, order status tracking
  4. Payment Integration - Payment validation and webhook handling
  5. AI-Powered Features - Product description generation, order summarization
  6. Configuration Management - Environment-based settings with secrets

Validation Requirements:

  • All external input validated at API layer
  • Business rules enforced at service layer
  • Database constraints at repository layer
  • Custom types for domain concepts (Money, SKU, Address)
  • Discriminated unions for polymorphic data (PaymentMethod, WebhookEvent)
  • Generic response wrappers for consistent API responses

API Endpoints

Authentication:
  POST   /auth/register          Register new user
  POST   /auth/login             Login and get tokens
  POST   /auth/refresh           Refresh access token

Users:
  GET    /users/me               Get current user profile
  PATCH  /users/me               Update profile
  GET    /users/me/orders        List user's orders

Products:
  GET    /products               List products (paginated, filtered)
  GET    /products/{sku}         Get product details
  POST   /products               Create product (admin)
  PATCH  /products/{sku}         Update product (admin)

Cart:
  GET    /cart                   Get current cart
  POST   /cart/items             Add item to cart
  PATCH  /cart/items/{item_id}   Update cart item quantity
  DELETE /cart/items/{item_id}   Remove item from cart

Orders:
  POST   /orders                 Create order from cart
  GET    /orders/{order_id}      Get order details
  POST   /orders/{order_id}/cancel  Cancel order

Payments:
  POST   /payments               Process payment
  GET    /payments/{payment_id}  Get payment status

Webhooks:
  POST   /webhooks/payment       Receive payment webhooks

AI Features:
  POST   /ai/product-description Generate product description
  POST   /ai/order-summary       Generate order summary

Domain Models

# Domain concepts that appear throughout the system

# Custom Types
Money              # amount + currency with arithmetic
SKU                # Product identifier (pattern: ABC-123456)
OrderId            # Order identifier (pattern: ORD-XXXXXXXXXX)
PhoneNumber        # E.164 normalized phone
Address            # Structured address with validation
PaymentMethod      # Discriminated union (card, bank, wallet)

# Core Entities
User               # id, email, name, phone, addresses
Product            # sku, name, description, price, inventory
ProductVariant     # size, color, additional price
CartItem           # product_sku, variant, quantity, unit_price
Cart               # user_id, items, totals
Order              # id, user, items, shipping, payment, status
Payment            # id, order_id, method, amount, status

# API Models (per entity)
{Entity}Create     # Required fields for creation
{Entity}Update     # Optional fields for updates
{Entity}Response   # Fields for API responses
{Entity}List       # Paginated list wrapper

Solution Architecture

Project Structure

production-app/
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ main.py                    # FastAPI application entry point
โ”‚   โ”œโ”€โ”€ dependencies.py            # Shared dependencies (auth, db)
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ config/
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ””โ”€โ”€ settings.py            # Pydantic Settings
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ types/                     # Custom domain types
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py            # Export all types
โ”‚   โ”‚   โ”œโ”€โ”€ money.py               # Money class
โ”‚   โ”‚   โ”œโ”€โ”€ phone.py               # PhoneNumber type
โ”‚   โ”‚   โ”œโ”€โ”€ address.py             # Address model
โ”‚   โ”‚   โ”œโ”€โ”€ identifiers.py         # SKU, OrderId, etc.
โ”‚   โ”‚   โ””โ”€โ”€ validators.py          # Shared validators
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ models/                    # Pydantic models per domain
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”œโ”€โ”€ base.py                # Base models, mixins
โ”‚   โ”‚   โ”œโ”€โ”€ user.py                # User models (all layers)
โ”‚   โ”‚   โ”œโ”€โ”€ product.py             # Product models
โ”‚   โ”‚   โ”œโ”€โ”€ cart.py                # Cart models
โ”‚   โ”‚   โ”œโ”€โ”€ order.py               # Order models
โ”‚   โ”‚   โ”œโ”€โ”€ payment.py             # Payment models (unions!)
โ”‚   โ”‚   โ”œโ”€โ”€ webhook.py             # Webhook event models
โ”‚   โ”‚   โ”œโ”€โ”€ ai.py                  # LLM structured outputs
โ”‚   โ”‚   โ”œโ”€โ”€ responses.py           # Generic response wrappers
โ”‚   โ”‚   โ””โ”€โ”€ errors.py              # Error response models
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ api/
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”œโ”€โ”€ routes/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ auth.py
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ users.py
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ products.py
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ cart.py
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ orders.py
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ payments.py
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ webhooks.py
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ ai.py
โ”‚   โ”‚   โ”‚
โ”‚   โ”‚   โ””โ”€โ”€ error_handlers.py      # Exception handlers
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ services/                  # Business logic
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”œโ”€โ”€ user_service.py
โ”‚   โ”‚   โ”œโ”€โ”€ product_service.py
โ”‚   โ”‚   โ”œโ”€โ”€ cart_service.py
โ”‚   โ”‚   โ”œโ”€โ”€ order_service.py
โ”‚   โ”‚   โ”œโ”€โ”€ payment_service.py
โ”‚   โ”‚   โ””โ”€โ”€ ai_service.py          # LLM integration
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ db/
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”œโ”€โ”€ session.py             # Database connection
โ”‚   โ”‚   โ”œโ”€โ”€ models.py              # SQLModel table models
โ”‚   โ”‚   โ””โ”€โ”€ repositories/
โ”‚   โ”‚       โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚       โ”œโ”€โ”€ base.py            # Generic repository
โ”‚   โ”‚       โ”œโ”€โ”€ user_repo.py
โ”‚   โ”‚       โ”œโ”€โ”€ product_repo.py
โ”‚   โ”‚       โ”œโ”€โ”€ cart_repo.py
โ”‚   โ”‚       โ””โ”€โ”€ order_repo.py
โ”‚   โ”‚
โ”‚   โ””โ”€โ”€ utils/
โ”‚       โ”œโ”€โ”€ __init__.py
โ”‚       โ”œโ”€โ”€ security.py            # Password hashing, JWT
โ”‚       โ””โ”€โ”€ pagination.py          # Pagination helpers
โ”‚
โ”œโ”€โ”€ tests/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ conftest.py                # Fixtures
โ”‚   โ”œโ”€โ”€ unit/
โ”‚   โ”‚   โ”œโ”€โ”€ test_types.py
โ”‚   โ”‚   โ”œโ”€โ”€ test_models.py
โ”‚   โ”‚   โ””โ”€โ”€ test_validators.py
โ”‚   โ”œโ”€โ”€ integration/
โ”‚   โ”‚   โ”œโ”€โ”€ test_api_users.py
โ”‚   โ”‚   โ”œโ”€โ”€ test_api_orders.py
โ”‚   โ”‚   โ””โ”€โ”€ test_services.py
โ”‚   โ””โ”€โ”€ e2e/
โ”‚       โ””โ”€โ”€ test_checkout_flow.py
โ”‚
โ”œโ”€โ”€ alembic/                       # Database migrations
โ”‚   โ”œโ”€โ”€ versions/
โ”‚   โ””โ”€โ”€ env.py
โ”‚
โ”œโ”€โ”€ pyproject.toml
โ”œโ”€โ”€ docker-compose.yml
โ”œโ”€โ”€ Dockerfile
โ””โ”€โ”€ README.md

Component Interaction Diagram

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                              EXTERNAL                                       โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”              โ”‚
โ”‚   โ”‚  Mobile   โ”‚  โ”‚    Web    โ”‚  โ”‚  Webhook  โ”‚  โ”‚ LLM API   โ”‚              โ”‚
โ”‚   โ”‚   App     โ”‚  โ”‚  Client   โ”‚  โ”‚  Events   โ”‚  โ”‚ (OpenAI)  โ”‚              โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
          โ”‚              โ”‚              โ”‚              โ”‚
          โ–ผ              โ–ผ              โ–ผ              โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                           API LAYER (FastAPI)                               โ”‚
โ”‚                                                                             โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚   โ”‚  /auth   โ”‚  โ”‚ /users   โ”‚  โ”‚ /orders  โ”‚  โ”‚/webhooks โ”‚  โ”‚   /ai    โ”‚   โ”‚
โ”‚   โ”‚          โ”‚  โ”‚          โ”‚  โ”‚          โ”‚  โ”‚          โ”‚  โ”‚          โ”‚   โ”‚
โ”‚   โ”‚ UserAuth โ”‚  โ”‚UserCreateโ”‚  โ”‚OrderReq  โ”‚  โ”‚ Webhook  โ”‚  โ”‚ AIQuery  โ”‚   โ”‚
โ”‚   โ”‚ Response โ”‚  โ”‚UserResp  โ”‚  โ”‚OrderResp โ”‚  โ”‚ Event    โ”‚  โ”‚AIOutput  โ”‚   โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚        โ”‚             โ”‚             โ”‚             โ”‚             โ”‚          โ”‚
โ”‚        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜          โ”‚
โ”‚                      โ–ผ             โ–ผ             โ–ผ                        โ”‚
โ”‚              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”              โ”‚
โ”‚              โ”‚           Error Handler                      โ”‚              โ”‚
โ”‚              โ”‚  ValidationError โ†’ ErrorResponse            โ”‚              โ”‚
โ”‚              โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                   โ”‚
                                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                         SERVICE LAYER                                       โ”‚
โ”‚                                                                             โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚   โ”‚ UserService  โ”‚  โ”‚ OrderService โ”‚  โ”‚PaymentServiceโ”‚  โ”‚  AIService   โ”‚  โ”‚
โ”‚   โ”‚              โ”‚  โ”‚              โ”‚  โ”‚              โ”‚  โ”‚              โ”‚  โ”‚
โ”‚   โ”‚ - validate   โ”‚  โ”‚ - validate   โ”‚  โ”‚ - validate   โ”‚  โ”‚ - generate   โ”‚  โ”‚
โ”‚   โ”‚   business   โ”‚  โ”‚   inventory  โ”‚  โ”‚   payment    โ”‚  โ”‚   content    โ”‚  โ”‚
โ”‚   โ”‚   rules      โ”‚  โ”‚ - calculate  โ”‚  โ”‚ - process    โ”‚  โ”‚ - parse LLM  โ”‚  โ”‚
โ”‚   โ”‚              โ”‚  โ”‚   totals     โ”‚  โ”‚   webhooks   โ”‚  โ”‚   output     โ”‚  โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚          โ”‚                 โ”‚                 โ”‚                 โ”‚          โ”‚
โ”‚          โ”‚    Domain Models (with custom types)                โ”‚          โ”‚
โ”‚          โ”‚    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”              โ”‚          โ”‚
โ”‚          โ”œโ”€โ”€โ”€โ–บโ”‚ Money, Address, PaymentMethod   โ”‚โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค          โ”‚
โ”‚          โ”‚    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜              โ”‚          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
           โ”‚                                                     โ”‚
           โ–ผ                                                     โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                       REPOSITORY LAYER                                      โ”‚
โ”‚                                                                             โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                    โ”‚
โ”‚   โ”‚  UserRepo    โ”‚  โ”‚  OrderRepo   โ”‚  โ”‚ ProductRepo  โ”‚                    โ”‚
โ”‚   โ”‚              โ”‚  โ”‚              โ”‚  โ”‚              โ”‚                    โ”‚
โ”‚   โ”‚ - SQLModel   โ”‚  โ”‚ - SQLModel   โ”‚  โ”‚ - SQLModel   โ”‚                    โ”‚
โ”‚   โ”‚ - CRUD ops   โ”‚  โ”‚ - CRUD ops   โ”‚  โ”‚ - CRUD ops   โ”‚                    โ”‚
โ”‚   โ”‚ - Relations  โ”‚  โ”‚ - Relations  โ”‚  โ”‚ - Relations  โ”‚                    โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                    โ”‚
โ”‚          โ”‚                 โ”‚                 โ”‚                             โ”‚
โ”‚          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                             โ”‚
โ”‚                            โ–ผ                                               โ”‚
โ”‚                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                       โ”‚
โ”‚                    โ”‚   Session     โ”‚                                       โ”‚
โ”‚                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                       โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                             โ”‚
                             โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                          INFRASTRUCTURE                                     โ”‚
โ”‚                                                                             โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                 โ”‚
โ”‚   โ”‚  PostgreSQL   โ”‚  โ”‚    Redis      โ”‚  โ”‚   OpenAI      โ”‚                 โ”‚
โ”‚   โ”‚  (Primary DB) โ”‚  โ”‚   (Cache)     โ”‚  โ”‚  (LLM API)    โ”‚                 โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                 โ”‚
โ”‚                                                                             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Phased Implementation Guide

Phase 1: Project Foundation (Day 1)

Goal: Set up project structure with settings and basic types.

  1. Create project structure with all directories
  2. Install dependencies:
    pip install fastapi uvicorn pydantic pydantic-settings sqlmodel \
                alembic asyncpg redis instructor openai python-jose passlib
    
  3. Implement settings management:
    # app/config/settings.py
    from pydantic_settings import BaseSettings, SettingsConfigDict
    from pydantic import Field, SecretStr, PostgresDsn, model_validator
    from typing import Literal
    
    class Settings(BaseSettings):
        # Application
        app_name: str = "Production App"
        environment: Literal["dev", "staging", "prod"] = "dev"
        debug: bool = False
        version: str = "1.0.0"
    
        # Database
        database_url: PostgresDsn
        database_pool_size: int = Field(5, ge=1, le=50)
    
        # Redis
        redis_url: str = "redis://localhost:6379"
    
        # Security
        secret_key: SecretStr
        access_token_expire_minutes: int = 30
        refresh_token_expire_days: int = 7
    
        # LLM
        openai_api_key: SecretStr
        openai_model: str = "gpt-4"
    
        model_config = SettingsConfigDict(
            env_file=".env",
            env_prefix="APP_",
            case_sensitive=False,
        )
    
        @model_validator(mode='after')
        def validate_production(self):
            if self.environment == "prod":
                if self.debug:
                    raise ValueError("Debug must be False in production")
            return self
    
    settings = Settings()
    
  4. Create base custom types:
    # app/types/money.py
    from decimal import Decimal
    from pydantic import GetCoreSchemaHandler
    from pydantic_core import CoreSchema, core_schema
    from typing import Any
    
    class Money:
        __slots__ = ('amount', 'currency')
    
        def __init__(self, amount: Decimal | str | float, currency: str = "USD"):
            self.amount = Decimal(str(amount)).quantize(Decimal("0.01"))
            self.currency = currency.upper()
    
        def __repr__(self):
            return f"{self.currency} {self.amount}"
    
        def __add__(self, other: "Money") -> "Money":
            if self.currency != other.currency:
                raise ValueError("Cannot add different currencies")
            return Money(self.amount + other.amount, self.currency)
    
        @classmethod
        def __get_pydantic_core_schema__(
            cls, _source: type, handler: GetCoreSchemaHandler
        ) -> CoreSchema:
            return core_schema.no_info_after_validator_function(
                cls._validate,
                core_schema.union_schema([
                    core_schema.is_instance_schema(Money),
                    core_schema.dict_schema(),
                    core_schema.str_schema(),
                    core_schema.float_schema(),
                ])
            )
    
        @classmethod
        def _validate(cls, value: Any) -> "Money":
            if isinstance(value, Money):
                return value
            if isinstance(value, dict):
                return cls(value["amount"], value.get("currency", "USD"))
            return cls(value)
    

Checkpoint: Settings load from .env and custom types work.

Phase 2: Core Models and Types (Day 2-3)

Goal: Create all domain models and custom types.

  1. Create custom types:
    • PhoneNumber (Annotated type with normalization)
    • Address (BaseModel with state/postal validation)
    • SKU, OrderId (Annotated pattern types)
  2. Create base models and mixins:
    # app/models/base.py
    from pydantic import BaseModel, ConfigDict
    from datetime import datetime
    
    class TimestampMixin(BaseModel):
        created_at: datetime
        updated_at: datetime | None = None
    
    class APIModelConfig:
        """Standard config for API models"""
        model_config = ConfigDict(
            from_attributes=True,
            populate_by_name=True,
            str_strip_whitespace=True,
        )
    
  3. Implement all domain models (User, Product, Cart, Order, Payment)

  4. Create generic response wrappers:
    # app/models/responses.py
    from pydantic import BaseModel
    from typing import Generic, TypeVar
    
    T = TypeVar('T')
    
    class APIResponse(BaseModel, Generic[T]):
        success: bool = True
        data: T | None = None
        message: str | None = None
    
    class PaginatedResponse(BaseModel, Generic[T]):
        items: list[T]
        total: int
        page: int
        per_page: int
        pages: int
    

Checkpoint: All models import and validate correctly.

Phase 3: Database Layer (Day 4-5)

Goal: Implement SQLModel tables and repositories.

  1. Create SQLModel table definitions:
    # app/db/models.py
    from sqlmodel import SQLModel, Field, Relationship
    from typing import Optional
    from datetime import datetime
    
    class UserDB(SQLModel, table=True):
        __tablename__ = "users"
    
        id: int | None = Field(default=None, primary_key=True)
        email: str = Field(unique=True, index=True)
        name: str
        hashed_password: str
        phone: str | None = None
        created_at: datetime = Field(default_factory=datetime.utcnow)
    
        orders: list["OrderDB"] = Relationship(back_populates="user")
    
  2. Set up database session:
    # app/db/session.py
    from sqlmodel import create_engine, Session
    from app.config.settings import settings
    
    engine = create_engine(str(settings.database_url))
    
    def get_session():
        with Session(engine) as session:
            yield session
    
  3. Implement repository pattern:
    # app/db/repositories/base.py
    from typing import Generic, TypeVar, Type
    from sqlmodel import Session, SQLModel, select
    
    ModelType = TypeVar("ModelType", bound=SQLModel)
    
    class BaseRepository(Generic[ModelType]):
        def __init__(self, session: Session, model: Type[ModelType]):
            self.session = session
            self.model = model
    
        def get(self, id: int) -> ModelType | None:
            return self.session.get(self.model, id)
    
        def list(self, skip: int = 0, limit: int = 100) -> list[ModelType]:
            stmt = select(self.model).offset(skip).limit(limit)
            return self.session.exec(stmt).all()
    
        def create(self, obj: ModelType) -> ModelType:
            self.session.add(obj)
            self.session.commit()
            self.session.refresh(obj)
            return obj
    
  4. Set up Alembic migrations

Checkpoint: Database operations work with migrations.

Phase 4: Service Layer (Day 6-7)

Goal: Implement business logic with validation.

  1. User service with authentication:
    # app/services/user_service.py
    from app.models.user import UserCreate, UserResponse
    from app.db.repositories.user_repo import UserRepository
    from app.utils.security import hash_password, verify_password
    
    class UserService:
        def __init__(self, repo: UserRepository):
            self.repo = repo
    
        def create_user(self, user_data: UserCreate) -> UserResponse:
            # Check email uniqueness
            if self.repo.get_by_email(user_data.email):
                raise EmailAlreadyExistsError(user_data.email)
    
            # Create user
            hashed_pw = hash_password(user_data.password)
            user = self.repo.create(
                email=user_data.email,
                name=user_data.name,
                hashed_password=hashed_pw,
            )
            return UserResponse.model_validate(user)
    
  2. Order service with inventory validation:
    # app/services/order_service.py
    class OrderService:
        def create_order(self, user_id: int, cart: Cart) -> Order:
            # Validate inventory for all items
            for item in cart.items:
                product = self.product_repo.get(item.sku)
                if product.inventory < item.quantity:
                    raise InsufficientInventoryError(item.sku, item.quantity)
    
            # Calculate totals
            subtotal = sum(item.subtotal for item in cart.items)
            tax = self.calculate_tax(subtotal, cart.shipping_address)
            total = subtotal + tax
    
            # Create order
            order = Order(
                user_id=user_id,
                items=cart.items,
                subtotal=subtotal,
                tax=tax,
                total=total,
                status="pending"
            )
            return self.order_repo.create(order)
    
  3. Payment service with webhook handling (discriminated unions)

Checkpoint: Services enforce business rules.

Phase 5: API Layer (Day 8-9)

Goal: Implement all API endpoints with validation.

  1. Create error handlers:
    # app/api/error_handlers.py
    from fastapi import FastAPI, Request
    from fastapi.exceptions import RequestValidationError
    from pydantic import ValidationError
    from app.models.errors import ErrorResponse
    
    def register_handlers(app: FastAPI):
        @app.exception_handler(RequestValidationError)
        async def validation_handler(request: Request, exc: RequestValidationError):
            errors = []
            for error in exc.errors():
                errors.append({
                    "field": ".".join(str(loc) for loc in error["loc"]),
                    "message": error["msg"],
                    "type": error["type"],
                })
            return JSONResponse(
                status_code=422,
                content=ErrorResponse(
                    success=False,
                    code="VALIDATION_ERROR",
                    message="Request validation failed",
                    errors=errors,
                ).model_dump()
            )
    
  2. Implement authentication middleware

  3. Create all route handlers with proper models

  4. Add request ID tracking:
    @app.middleware("http")
    async def add_request_id(request: Request, call_next):
        request_id = request.headers.get("X-Request-ID", str(uuid4()))
        response = await call_next(request)
        response.headers["X-Request-ID"] = request_id
        return response
    

Checkpoint: All endpoints work with proper validation.

Phase 6: Payment Webhooks with Discriminated Unions (Day 10)

Goal: Handle polymorphic webhook events.

  1. Define webhook event models:
    # app/models/webhook.py
    from pydantic import BaseModel, Field
    from typing import Literal, Union, Annotated
    
    class PaymentSucceededEvent(BaseModel):
        type: Literal["payment.succeeded"]
        payment_id: str
        amount: Money
        order_id: str
    
    class PaymentFailedEvent(BaseModel):
        type: Literal["payment.failed"]
        payment_id: str
        error_code: str
        error_message: str
    
    class RefundCreatedEvent(BaseModel):
        type: Literal["refund.created"]
        refund_id: str
        payment_id: str
        amount: Money
    
    WebhookEvent = Annotated[
        Union[PaymentSucceededEvent, PaymentFailedEvent, RefundCreatedEvent],
        Field(discriminator="type")
    ]
    
  2. Implement webhook handler:
    @router.post("/webhooks/payment")
    async def handle_payment_webhook(
        event: WebhookEvent,
        x_signature: str = Header(...)
    ):
        # Verify webhook signature
        if not verify_signature(event, x_signature):
            raise HTTPException(401, "Invalid signature")
    
        # Dispatch based on event type
        match event:
            case PaymentSucceededEvent():
                await process_payment_success(event)
            case PaymentFailedEvent():
                await process_payment_failure(event)
            case RefundCreatedEvent():
                await process_refund(event)
    
        return {"status": "processed"}
    

Checkpoint: Webhook events parse correctly by type.

Phase 7: LLM Integration (Day 11-12)

Goal: Add AI-powered features with structured outputs.

  1. Define structured output models:
    # app/models/ai.py
    from pydantic import BaseModel, Field
    
    class ProductDescription(BaseModel):
        """Structured product description from LLM"""
        headline: str = Field(max_length=100)
        short_description: str = Field(max_length=250)
        features: list[str] = Field(min_length=3, max_length=5)
        target_audience: str
        seo_keywords: list[str]
    
    class OrderSummary(BaseModel):
        """Natural language order summary"""
        summary: str = Field(max_length=500)
        highlights: list[str]
        estimated_delivery: str
        next_steps: list[str]
    
  2. Implement AI service:
    # app/services/ai_service.py
    import instructor
    from openai import OpenAI
    from app.config.settings import settings
    
    class AIService:
        def __init__(self):
            self.client = instructor.from_openai(
                OpenAI(api_key=settings.openai_api_key.get_secret_value())
            )
    
        def generate_product_description(
            self,
            product_name: str,
            category: str,
            features: list[str]
        ) -> ProductDescription:
            return self.client.chat.completions.create(
                model=settings.openai_model,
                response_model=ProductDescription,
                messages=[{
                    "role": "user",
                    "content": f"""Generate a product description for:
                    Name: {product_name}
                    Category: {category}
                    Features: {', '.join(features)}
                    """
                }]
            )
    

Checkpoint: LLM returns validated structured data.

Phase 8: Testing and Production Readiness (Day 13-14)

Goal: Comprehensive tests and production configuration.

  1. Write unit tests for types and models:
    # tests/unit/test_types.py
    def test_money_arithmetic():
        m1 = Money("10.00", "USD")
        m2 = Money("5.50", "USD")
        assert (m1 + m2).amount == Decimal("15.50")
    
    def test_money_different_currencies_raises():
        m1 = Money("10.00", "USD")
        m2 = Money("10.00", "EUR")
        with pytest.raises(ValueError):
            m1 + m2
    
  2. Write integration tests for API:
    # tests/integration/test_api_orders.py
    def test_create_order_validates_inventory():
        # Add product with limited inventory
        product = create_product(inventory=5)
    
        # Try to order more than available
        response = client.post("/orders", json={
            "items": [{"sku": product.sku, "quantity": 10}]
        })
    
        assert response.status_code == 422
        assert "inventory" in response.json()["errors"][0]["message"].lower()
    
  3. Add observability:
    • Structured logging with request IDs
    • Validation error metrics
    • Performance tracing
  4. Create Docker configuration:
    FROM python:3.11-slim
    
    WORKDIR /app
    COPY pyproject.toml .
    RUN pip install .
    COPY app/ app/
    
    CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0"]
    
  5. Create docker-compose for local development

Checkpoint: Tests pass, application runs in Docker.


Testing Strategy

Unit Tests

# tests/unit/test_models.py
import pytest
from pydantic import ValidationError
from app.models.user import UserCreate, UserUpdate
from app.types.money import Money
from app.types.phone import normalize_phone

class TestUserModels:
    def test_user_create_valid(self):
        user = UserCreate(
            email="test@example.com",
            name="Test User",
            password="securepassword123"
        )
        assert user.email == "test@example.com"

    def test_user_create_invalid_email(self):
        with pytest.raises(ValidationError) as exc:
            UserCreate(email="invalid", name="Test", password="password123")
        assert "email" in str(exc.value)

    def test_user_update_all_optional(self):
        update = UserUpdate()
        assert update.email is None
        assert update.name is None

class TestMoneyType:
    def test_parse_string(self):
        money = Money("99.99")
        assert money.amount == Decimal("99.99")
        assert money.currency == "USD"

    def test_parse_dict(self):
        money = Money._validate({"amount": "50.00", "currency": "EUR"})
        assert money.amount == Decimal("50.00")
        assert money.currency == "EUR"

    def test_quantize_to_cents(self):
        money = Money("99.999")
        assert money.amount == Decimal("100.00")

class TestPhoneNumber:
    @pytest.mark.parametrize("input,expected", [
        ("(555) 123-4567", "+15551234567"),
        ("5551234567", "+15551234567"),
        ("+1-555-123-4567", "+15551234567"),
        ("1-555-123-4567", "+15551234567"),
    ])
    def test_normalize_formats(self, input, expected):
        assert normalize_phone(input) == expected

    def test_invalid_phone_raises(self):
        with pytest.raises(ValueError):
            normalize_phone("123")  # Too short

Integration Tests

# tests/integration/test_api_users.py
import pytest
from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

class TestUserAPI:
    def test_register_user_success(self):
        response = client.post("/auth/register", json={
            "email": "new@example.com",
            "name": "New User",
            "password": "securepassword123"
        })
        assert response.status_code == 201
        data = response.json()["data"]
        assert data["email"] == "new@example.com"
        assert "password" not in data
        assert "id" in data

    def test_register_duplicate_email(self):
        # First registration
        client.post("/auth/register", json={
            "email": "dupe@example.com",
            "name": "First",
            "password": "password123"
        })

        # Duplicate
        response = client.post("/auth/register", json={
            "email": "dupe@example.com",
            "name": "Second",
            "password": "password123"
        })
        assert response.status_code == 409

    def test_validation_error_format(self):
        response = client.post("/auth/register", json={
            "email": "invalid",
            "name": "",
            "password": "short"
        })
        assert response.status_code == 422
        errors = response.json()["errors"]
        assert len(errors) >= 3

        # Check error structure
        email_error = next(e for e in errors if "email" in e["field"])
        assert "message" in email_error
        assert "type" in email_error


# tests/integration/test_webhooks.py
class TestPaymentWebhooks:
    def test_payment_succeeded_event(self):
        response = client.post("/webhooks/payment", json={
            "type": "payment.succeeded",
            "payment_id": "pay_123",
            "amount": {"amount": "99.99", "currency": "USD"},
            "order_id": "ORD-ABC1234567"
        }, headers={"X-Signature": "valid_sig"})
        assert response.status_code == 200

    def test_payment_failed_event(self):
        response = client.post("/webhooks/payment", json={
            "type": "payment.failed",
            "payment_id": "pay_456",
            "error_code": "card_declined",
            "error_message": "Card was declined"
        }, headers={"X-Signature": "valid_sig"})
        assert response.status_code == 200

    def test_unknown_event_type(self):
        response = client.post("/webhooks/payment", json={
            "type": "unknown.event",
            "data": {}
        }, headers={"X-Signature": "valid_sig"})
        assert response.status_code == 422  # Validation error

End-to-End Tests

# tests/e2e/test_checkout_flow.py
import pytest
from tests.fixtures import create_test_user, create_test_product

class TestCheckoutFlow:
    @pytest.fixture(autouse=True)
    def setup(self, db_session):
        self.user = create_test_user(db_session)
        self.product = create_test_product(db_session, inventory=10)

    def test_complete_checkout(self, client):
        # 1. Login
        login_resp = client.post("/auth/login", json={
            "email": self.user.email,
            "password": "testpassword"
        })
        token = login_resp.json()["data"]["access_token"]
        headers = {"Authorization": f"Bearer {token}"}

        # 2. Add to cart
        cart_resp = client.post("/cart/items", json={
            "sku": self.product.sku,
            "quantity": 2
        }, headers=headers)
        assert cart_resp.status_code == 200

        # 3. Get cart
        cart = client.get("/cart", headers=headers).json()["data"]
        assert len(cart["items"]) == 1

        # 4. Create order
        order_resp = client.post("/orders", json={
            "shipping_address": {
                "street": "123 Main St",
                "city": "New York",
                "state": "NY",
                "postal_code": "10001",
                "country": "US"
            }
        }, headers=headers)
        assert order_resp.status_code == 201
        order = order_resp.json()["data"]

        # 5. Process payment
        payment_resp = client.post("/payments", json={
            "order_id": order["id"],
            "method": {"type": "card", "token": "tok_visa"}
        }, headers=headers)
        assert payment_resp.status_code == 200

        # 6. Verify order status
        order = client.get(f"/orders/{order['id']}", headers=headers).json()["data"]
        assert order["status"] == "confirmed"

        # 7. Verify inventory decreased
        product = client.get(f"/products/{self.product.sku}").json()["data"]
        assert product["inventory"] == 8

Common Pitfalls and Debugging

Pitfall 1: Circular Import with Model Dependencies

Problem: User model references Order, Order references User.

Symptom:

ImportError: cannot import name 'UserResponse' from partially initialized module

Solution:

# Use TYPE_CHECKING for type hints only
from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from app.models.order import OrderResponse

class UserResponse(BaseModel):
    orders: list["OrderResponse"] = []  # Forward reference as string

# Rebuild models after all imports
UserResponse.model_rebuild()

Pitfall 2: model_construct Bypasses Validation

Problem: Using model_construct with untrusted data.

Symptom: Invalid data gets into your system without errors.

Solution:

# NEVER use model_construct with external data
user = User.model_construct(**request.json())  # BAD!

# ONLY use with internal/trusted data
user = User.model_construct(**db_row)  # OK - already validated on insert

# When in doubt, use model_validate
user = User.model_validate(db_row)  # Safe

Pitfall 3: Discriminated Union Order Matters

Problem: Most specific types must come first in union.

Symptom: Always matches the first type in union.

Solution:

# BAD: Generic type first catches everything
PaymentMethod = Union[
    GenericPayment,  # Catches all!
    CardPayment,
    BankPayment,
]

# GOOD: Specific types first
PaymentMethod = Annotated[
    Union[
        CardPayment,
        BankPayment,
        GenericPayment,  # Fallback last
    ],
    Field(discriminator="type")
]

Pitfall 4: Settings Not Loading in Tests

Problem: Tests use different environment than expected.

Symptom: Tests fail with missing environment variables.

Solution:

# conftest.py
import pytest
from app.config.settings import Settings

@pytest.fixture(autouse=True)
def test_settings(monkeypatch):
    """Override settings for tests"""
    monkeypatch.setenv("APP_DATABASE_URL", "postgresql://test:test@localhost/test")
    monkeypatch.setenv("APP_SECRET_KEY", "test-secret-key")
    monkeypatch.setenv("APP_OPENAI_API_KEY", "test-key")

    # Force settings reload
    from app.config import settings
    settings.__init__()

Pitfall 5: SQLModel and Pydantic Model Confusion

Problem: Using SQLModel table classes as API response models.

Symptom: Relationships not loaded, or sensitive fields exposed.

Solution:

# SEPARATE your API models from DB models
# db/models.py
class UserDB(SQLModel, table=True):
    id: int | None = Field(primary_key=True)
    email: str
    hashed_password: str  # Sensitive!
    orders: list["OrderDB"] = Relationship(...)

# models/user.py
class UserResponse(BaseModel):
    id: int
    email: str
    # NO hashed_password!
    # Relationships converted to simple types

    model_config = ConfigDict(from_attributes=True)

Pitfall 6: Money Type Precision Loss

Problem: Floating point used for money calculations.

Symptom: 0.1 + 0.2 != 0.3 issues in totals.

Solution:

# ALWAYS use Decimal for money
from decimal import Decimal, ROUND_HALF_UP

class Money:
    def __init__(self, amount: str | float | Decimal):
        # Convert to string first to avoid float precision issues
        self.amount = Decimal(str(amount)).quantize(
            Decimal("0.01"),
            rounding=ROUND_HALF_UP
        )

# In models
total: Money  # Not float!

Extensions and Challenges

Extension 1: Multi-Currency Support

Add currency conversion to the Money type:

class Money:
    async def convert_to(self, target_currency: str) -> "Money":
        rate = await get_exchange_rate(self.currency, target_currency)
        return Money(self.amount * rate, target_currency)

class Order(BaseModel):
    currency: str = "USD"
    items: list[OrderItem]

    @computed_field
    def total_in_currency(self) -> Money:
        total = sum(item.subtotal for item in self.items)
        if item.currency != self.currency:
            return total.convert_to(self.currency)
        return total

Extension 2: Schema Versioning

Support multiple API versions with model evolution:

# models/v1/user.py
class UserResponseV1(BaseModel):
    id: int
    name: str

# models/v2/user.py
class UserResponseV2(BaseModel):
    id: int
    full_name: str  # Renamed
    email_verified: bool  # New field

# Adapter
def v1_to_v2(v1: UserResponseV1) -> UserResponseV2:
    return UserResponseV2(
        id=v1.id,
        full_name=v1.name,
        email_verified=False  # Default for migrated users
    )

Extension 3: Async Validation

Implement validators that require async operations:

from pydantic import model_validator

class OrderCreate(BaseModel):
    items: list[OrderItem]

    @model_validator(mode='after')
    async def validate_inventory_async(self):
        for item in self.items:
            available = await check_inventory_async(item.sku)
            if available < item.quantity:
                raise ValueError(f"Insufficient inventory for {item.sku}")
        return self

Extension 4: Feature Flags with Pydantic

Type-safe feature flags:

class FeatureFlags(BaseSettings):
    enable_ai_descriptions: bool = False
    enable_multi_currency: bool = False
    max_cart_items: int = Field(50, ge=1)

    model_config = SettingsConfigDict(env_prefix="FEATURE_")

# Usage
if settings.feature_flags.enable_ai_descriptions:
    description = ai_service.generate(product)

Extension 5: GraphQL Integration

Use Pydantic models with Strawberry GraphQL:

import strawberry
from app.models.user import UserResponse

@strawberry.experimental.pydantic.type(model=UserResponse)
class UserType:
    pass

@strawberry.type
class Query:
    @strawberry.field
    async def user(self, id: int) -> UserType:
        user = await user_service.get(id)
        return UserType.from_pydantic(user)

Real-World Connections

Industry Patterns This Project Demonstrates

  1. Clean Architecture - Separation of concerns across layers
  2. Domain-Driven Design - Rich domain models with behavior
  3. API-First Development - OpenAPI-driven contracts
  4. Event-Driven Architecture - Webhook event handling
  5. Repository Pattern - Decoupled data access

Companies Using These Patterns

  • Stripe - Webhook events with discriminated unions
  • Shopify - Order management with complex validation
  • Netflix - Pydantic for configuration management
  • Anthropic/OpenAI - Structured outputs from LLMs

Skills Developed

Skill Application
API Design Building RESTful APIs with proper validation
Type Systems Advanced Python typing for safety
Data Modeling Separating concerns across layers
Error Handling User-friendly, consistent errors
Testing Pyramid strategy with proper coverage
DevOps Containerization and configuration

Self-Assessment Checklist

Architecture Understanding

  • Can I explain why we separate API, Service, and Repository layers?
  • Can I describe the flow of data validation through all layers?
  • Can I explain when to use each model type (Input, Domain, Output)?
  • Can I describe the benefits of custom domain types?

Implementation Skills

  • Can I create custom Pydantic types with __get_pydantic_core_schema__?
  • Can I implement discriminated unions for polymorphic data?
  • Can I integrate Pydantic Settings for configuration?
  • Can I use SQLModel for database models?
  • Can I implement structured LLM outputs with Instructor?

Error Handling

  • Can I create custom exception handlers for FastAPI?
  • Can I aggregate and format errors across validation layers?
  • Can I provide helpful error messages for API consumers?

Performance

  • Do I know when to use model_construct vs model_validate?
  • Can I implement caching for validated objects?
  • Do I understand model_validate_json performance benefits?

Testing

  • Can I write unit tests for custom types and validators?
  • Can I write integration tests for API endpoints?
  • Can I write E2E tests for complete user flows?
  • Do I have >80% code coverage?

Production Readiness

  • Does my application run in Docker?
  • Are secrets properly managed with SecretStr?
  • Do I have proper logging and observability?
  • Are database migrations working?

Resources

Documentation

Books

  • โ€œArchitecture Patterns with Pythonโ€ by Harry Percival & Bob Gregory
  • โ€œFluent Pythonโ€ by Luciano Ramalho
  • โ€œRobust Pythonโ€ by Patrick Viafore
  • โ€œBuilding Data Science Applications with FastAPIโ€ by Francois Voron

Video Resources


This capstone project integrates everything learned in the Pydantic Data Validation Deep Dive. Completing it demonstrates mastery of data validation architecture in Python.