# Architecture Patterns & Design Decisions This document describes the architectural patterns and design decisions that must be followed throughout the codebase. > **Note:** These patterns are enforced automatically by `scripts/validate/validate_architecture.py`. Run the validator before committing code. --- ## Table of Contents 1. [Core Principles](#core-principles) 2. [Layered Architecture](#layered-architecture) 3. [API Endpoint Patterns](#api-endpoint-patterns) 4. [Service Layer Patterns](#service-layer-patterns) 5. [Model Patterns](#model-patterns) 6. [Exception Handling](#exception-handling) 7. [JavaScript Patterns](#javascript-patterns) 8. [Validation](#validation) --- ## Core Principles ### 1. Separation of Concerns **Each layer has specific responsibilities:** - **Routes/Endpoints**: HTTP handling, validation, authentication, response formatting - **Services**: Business logic, data processing, orchestration - **Models**: Data structure and persistence **❌ Bad Example - Business logic in endpoint:** ```python @router.post("/stores") async def create_store(store: StoreCreate, db: Session = Depends(get_db)): # ❌ BAD: Business logic in endpoint if db.query(Store).filter(Store.subdomain == store.subdomain).first(): raise HTTPException(status_code=409, detail="Store exists") db_store = Store(**store.dict()) db.add(db_store) db.commit() db.refresh(db_store) return db_store ``` **✅ Good Example - Delegated to service:** ```python @router.post("/stores", response_model=StoreResponse) async def create_store( store: StoreCreate, current_user: User = Depends(get_current_admin), db: Session = Depends(get_db) ): try: # ✅ GOOD: Delegate to service result = store_service.create_store(db, store) return result except StoreAlreadyExistsError as e: raise HTTPException(status_code=409, detail=str(e)) except Exception as e: logger.error(f"Failed to create store: {e}") raise HTTPException(status_code=500, detail="Internal server error") ``` ### 2. Type Safety Use Pydantic for API validation, SQLAlchemy for database models. ### 3. Proper Exception Handling Services throw domain exceptions, routes convert to HTTP responses. --- ## Layered Architecture ``` ┌─────────────────────────────────────────────────────────┐ │ API Layer │ │ (app/api/v1/**/*.py, app/routes/**/*.py) │ │ │ │ Responsibilities: │ │ - HTTP request/response handling │ │ - Authentication/Authorization │ │ - Input validation (Pydantic models) │ │ - Exception handling (domain → HTTP) │ │ │ │ ❌ Should NOT: │ │ - Contain business logic │ │ - Directly access database (except via services) │ │ - Raise domain exceptions │ └─────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────┐ │ Service Layer │ │ (app/services/**/*.py) │ │ │ │ Responsibilities: │ │ - Business logic │ │ - Data validation (business rules) │ │ - Database operations │ │ - Orchestration of multiple operations │ │ │ │ ❌ Should NOT: │ │ - Know about HTTP (no HTTPException) │ │ - Create database sessions (accept as parameter) │ │ - Handle HTTP-specific concerns │ └─────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────┐ │ Data Layer │ │ (app/models/**/*.py) │ │ │ │ Responsibilities: │ │ - Database schema (SQLAlchemy models) │ │ - API schemas (Pydantic models) │ │ - Data structure definitions │ │ │ │ ❌ Should NOT: │ │ - Mix SQLAlchemy and Pydantic in same class │ │ - Contain business logic │ └─────────────────────────────────────────────────────────┘ ``` --- ## API Endpoint Patterns ### Rule API-001: Use Pydantic Models **All endpoints MUST use Pydantic models for request/response.** ```python # ✅ GOOD: Pydantic models for type safety class StoreCreate(BaseModel): name: str = Field(..., max_length=200) subdomain: str = Field(..., max_length=100) is_active: bool = True class StoreResponse(BaseModel): id: int name: str subdomain: str created_at: datetime class Config: from_attributes = True # For SQLAlchemy compatibility @router.post("/stores", response_model=StoreResponse) async def create_store(store: StoreCreate, db: Session = Depends(get_db)): result = store_service.create_store(db, store) return result ``` ```python # ❌ BAD: Raw dict, no validation @router.post("/stores") async def create_store(data: dict): return {"name": data["name"]} # No type safety! ``` ### Rule API-002: No Business Logic in Endpoints **Endpoints should only handle HTTP concerns.** ```python # ✅ GOOD: Delegate to service @router.post("/stores") async def create_store(store: StoreCreate, db: Session = Depends(get_db)): result = store_service.create_store(db, store) return result ``` ```python # ❌ BAD: Business logic in endpoint @router.post("/stores") async def create_store(store: StoreCreate, db: Session = Depends(get_db)): # ❌ Database operations belong in service! db_store = Store(**store.dict()) db.add(db_store) db.commit() return db_store ``` ### Rule API-003: Proper Exception Handling **Catch service exceptions and convert to HTTPException.** ```python # ✅ GOOD: Proper exception handling @router.post("/stores", response_model=StoreResponse) async def create_store(store: StoreCreate, db: Session = Depends(get_db)): try: result = store_service.create_store(db, store) return result except StoreAlreadyExistsError as e: raise HTTPException(status_code=409, detail=str(e)) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error(f"Unexpected error creating store: {e}") raise HTTPException(status_code=500, detail="Internal server error") ``` ### Rule API-004: Authentication **Protected endpoints must use dependency injection for auth.** ```python # ✅ GOOD: Use Depends for auth @router.post("/stores") async def create_store( store: StoreCreate, current_user: User = Depends(get_current_admin), # ✅ Auth required db: Session = Depends(get_db) ): result = store_service.create_store(db, store) return result ``` ### Rule API-005: Store Context from Token (Not URL) **Store API endpoints MUST extract store context from JWT token, NOT from URL.** > **Rationale:** Embedding store context in JWT tokens enables clean RESTful API endpoints, eliminates URL-based store detection issues, and improves security by cryptographically signing store access. **❌ BAD: URL-based store detection** ```python from middleware.store_context import require_store_context @router.get("/products") def get_products( store: Store = Depends(require_store_context()), # ❌ Requires store in URL current_user: User = Depends(get_current_store_api), db: Session = Depends(get_db), ): # This fails on /api/v1/store/products (no store in URL) products = product_service.get_store_products(db, store.id) return products ``` **Issues with URL-based approach:** - ❌ Only works with routes like `/store/{store_code}/dashboard` - ❌ Fails on API routes like `/api/v1/store/products` (no store in URL) - ❌ Inconsistent between page routes and API routes - ❌ Violates RESTful API design - ❌ Requires database lookup on every request **✅ GOOD: Token-based store context** ```python @router.get("/products") def get_products( current_user: User = Depends(get_current_store_api), # ✅ Store in token db: Session = Depends(get_db), ): # Extract store from JWT token if not hasattr(current_user, "token_store_id"): raise HTTPException( status_code=400, detail="Token missing store information. Please login again.", ) store_id = current_user.token_store_id # Use store_id from token products = product_service.get_store_products(db, store_id) return products ``` **Benefits of token-based approach:** - ✅ Works on all routes (page and API) - ✅ Clean RESTful API endpoints - ✅ Store context cryptographically signed in JWT - ✅ No database lookup needed for store detection - ✅ Consistent authentication mechanism - ✅ Security: Cannot be tampered with by client **Token structure:** ```json { "sub": "user_id", "username": "john.doe", "store_id": 123, ← Store context "store_code": "WIZAMART", ← Store code "store_role": "Owner" ← Store role } ``` **Available token attributes:** - `current_user.token_store_id` - Store ID (use for database queries) - `current_user.token_store_code` - Store code (use for logging) - `current_user.token_store_role` - Store role (Owner, Manager, etc.) **Migration checklist:** 1. Remove `store: Store = Depends(require_store_context())` 2. Remove unused imports: `from middleware.store_context import require_store_context` 3. Extract store from token: `store_id = current_user.token_store_id` 4. Add token validation check (see example above) 5. Update logging to use `current_user.token_store_code` **See also:** `docs/backend/store-in-token-architecture.md` for complete migration guide **Files requiring migration:** - `app/api/v1/store/customers.py` - `app/api/v1/store/notifications.py` - `app/api/v1/store/media.py` - `app/api/v1/store/marketplace.py` - `app/api/v1/store/inventory.py` - `app/api/v1/store/settings.py` - `app/api/v1/store/analytics.py` - `app/api/v1/store/payments.py` - `app/api/v1/store/profile.py` --- ## Service Layer Patterns ### Rule SVC-001: No HTTPException in Services **Services should NOT know about HTTP. Raise domain exceptions instead.** ```python # ✅ GOOD: Domain exception class StoreAlreadyExistsError(Exception): """Raised when store with same subdomain already exists""" pass class StoreService: def create_store(self, db: Session, store_data: StoreCreate): if self._store_exists(db, store_data.subdomain): raise StoreAlreadyExistsError( f"Store with subdomain '{store_data.subdomain}' already exists" ) # Business logic... store = Store(**store_data.dict()) db.add(store) db.commit() return store ``` ```python # ❌ BAD: HTTPException in service class StoreService: def create_store(self, db: Session, store_data: StoreCreate): if self._store_exists(db, store_data.subdomain): # ❌ Service shouldn't know about HTTP! raise HTTPException(status_code=409, detail="Store exists") ``` ### Rule SVC-002: Create Custom Exception Classes **Don't use generic `Exception`. Create specific domain exceptions.** ```python # ✅ GOOD: Specific exceptions class StoreError(Exception): """Base exception for store-related errors""" pass class StoreNotFoundError(StoreError): """Raised when store is not found""" pass class StoreAlreadyExistsError(StoreError): """Raised when store already exists""" pass class StoreService: def get_store(self, db: Session, store_code: str): store = db.query(Store).filter(Store.store_code == store_code).first() if not store: raise StoreNotFoundError(f"Store '{store_code}' not found") return store ``` ```python # ❌ BAD: Generic Exception class StoreService: def get_store(self, db: Session, store_code: str): store = db.query(Store).filter(Store.store_code == store_code).first() if not store: raise Exception("Store not found") # ❌ Too generic! return store ``` ### Rule SVC-003: Database Session as Parameter **Services should receive database session as parameter, not create it internally.** ```python # ✅ GOOD: db session as parameter class StoreService: def create_store(self, db: Session, store_data: StoreCreate): store = Store(**store_data.dict()) db.add(store) db.commit() db.refresh(store) return store def _store_exists(self, db: Session, subdomain: str) -> bool: return db.query(Store).filter(Store.subdomain == subdomain).first() is not None ``` ```python # ❌ BAD: Creating session internally class StoreService: def create_store(self, store_data: StoreCreate): # ❌ Don't create session here - makes testing hard db = SessionLocal() store = Store(**store_data.dict()) db.add(store) db.commit() return store ``` **Benefits:** - Testability (can inject mock session) - Transaction control (caller controls commit/rollback) - Resource management (caller handles session lifecycle) ### Rule SVC-004: Use Pydantic for Input Validation **Service methods should accept Pydantic models for complex inputs.** ```python # ✅ GOOD: Pydantic model ensures validation class StoreService: def create_store(self, db: Session, store_data: StoreCreate): # store_data is already validated by Pydantic store = Store(**store_data.dict()) db.add(store) db.commit() return store ``` --- ## Model Patterns ### Rule MDL-001: SQLAlchemy for Database Models ```python # ✅ GOOD: SQLAlchemy model from sqlalchemy import Column, Integer, String, Boolean from app.database import Base class Store(Base): __tablename__ = "stores" id = Column(Integer, primary_key=True, index=True) name = Column(String(200), nullable=False) subdomain = Column(String(100), unique=True, nullable=False) is_active = Column(Boolean, default=True) ``` ### Rule MDL-002: Separate Pydantic from SQLAlchemy **NEVER mix SQLAlchemy and Pydantic in the same class.** ```python # ❌ BAD: Mixing SQLAlchemy and Pydantic class Store(Base, BaseModel): # ❌ Don't do this! __tablename__ = "stores" name: str = Column(String(200)) ``` ```python # ✅ GOOD: Separate models # Database model (app/models/store.py) class Store(Base): __tablename__ = "stores" id = Column(Integer, primary_key=True) name = Column(String(200), nullable=False) # API model (app/api/v1/admin/stores.py) class StoreCreate(BaseModel): name: str = Field(..., max_length=200) class StoreResponse(BaseModel): id: int name: str class Config: from_attributes = True ``` --- ## Exception Handling ### Rule EXC-001: Domain-Specific Exceptions **Create exception hierarchy in `app/exceptions/`** ```python # app/exceptions/store_exceptions.py class StoreError(Exception): """Base exception for store domain""" pass class StoreNotFoundError(StoreError): """Store does not exist""" pass class StoreAlreadyExistsError(StoreError): """Store already exists""" pass class StoreValidationError(StoreError): """Store data validation failed""" pass ``` ### Rule EXC-002: Never Use Bare Except ```python # ❌ BAD: Bare except try: result = do_something() except: # ❌ Catches EVERYTHING including KeyboardInterrupt! pass ``` ```python # ✅ GOOD: Specific exceptions try: result = do_something() except ValueError as e: logger.error(f"Validation error: {e}") except DatabaseError as e: logger.error(f"Database error: {e}") except Exception as e: logger.error(f"Unexpected error: {e}") raise ``` --- ## JavaScript Patterns ### Rule JS-001: Use apiClient Directly ```javascript // ✅ GOOD const stores = await apiClient.get('/api/v1/stores'); ``` ```javascript // ❌ BAD const stores = await window.apiClient.get('/api/v1/stores'); ``` ### Rule JS-002: Use Centralized Logger ```javascript // ✅ GOOD: Centralized logger const storeLog = window.LogConfig.createLogger('stores'); storeLog.info('Loading stores...'); storeLog.error('Failed to load stores:', error); ``` ```javascript // ❌ BAD: console console.log('Loading stores...'); // ❌ Use logger instead ``` ### Rule JS-003: Alpine Components Pattern ```javascript // ✅ GOOD: Proper Alpine component function storesManager() { return { // ✅ Inherit base layout functionality ...data(), // ✅ Set page identifier for sidebar currentPage: 'stores', // Component state stores: [], loading: false, // ✅ Init with guard async init() { if (window._storesInitialized) { return; } window._storesInitialized = true; await this.loadStores(); } }; } ``` --- ## Validation ### Running the Validator ```bash # Validate entire codebase python scripts/validate/validate_architecture.py # Validate specific directory python scripts/validate/validate_architecture.py app/api/ # Verbose output with context python scripts/validate/validate_architecture.py --verbose # Errors only (suppress warnings) python scripts/validate/validate_architecture.py --errors-only ``` ### Pre-commit Hook Install pre-commit to validate automatically before commits: ```bash # Install pre-commit pip install pre-commit # Setup hooks pre-commit install # Run manually pre-commit run --all-files ``` ### CI/CD Integration Add to your CI pipeline: ```yaml # .github/workflows/ci.yml - name: Validate Architecture run: | python scripts/validate/validate_architecture.py ``` --- ## Quick Reference | Layer | Responsibility | Can Use | Cannot Use | |-------|---------------|---------|------------| | **API Endpoints** | HTTP handling, auth, validation | Pydantic, HTTPException, Depends | Direct DB access, business logic | | **Services** | Business logic, orchestration | DB session, domain exceptions | HTTPException, HTTP concepts | | **Models** | Data structure | SQLAlchemy OR Pydantic | Mixing both in same class | --- ## Common Violations and Fixes ### Violation: Business logic in endpoint ```python # Before @router.post("/stores") async def create_store(store: StoreCreate, db: Session = Depends(get_db)): db_store = Store(**store.dict()) db.add(db_store) db.commit() return db_store ``` ```python # After ✅ @router.post("/stores") async def create_store(store: StoreCreate, db: Session = Depends(get_db)): try: return store_service.create_store(db, store) except StoreAlreadyExistsError as e: raise HTTPException(status_code=409, detail=str(e)) ``` ### Violation: HTTPException in service ```python # Before class StoreService: def create_store(self, db: Session, store_data): if exists: raise HTTPException(status_code=409, detail="Exists") ``` ```python # After ✅ class StoreService: def create_store(self, db: Session, store_data): if exists: raise StoreAlreadyExistsError("Store already exists") ``` --- **Remember:** These patterns are enforced automatically. Run `python scripts/validate/validate_architecture.py` before committing!