# Domain Models — Reference Guide This document explains the domain model pattern used in BanGUI's backend and where to find examples. --- ## What Are Domain Models? Domain models (e.g., `DomainActiveBanList`, `DomainJailConfig`) are **frozen dataclasses** that represent pure business logic. They are defined in `app/models/{domain}_domain.py` and are **returned by services**. Response models (e.g., `ActiveBanListResponse`, `JailConfigResponse`) are **Pydantic models** defined in `app/models/{domain}.py`. They are used **only by routers** for HTTP serialization. --- ## Why This Separation? ``` Service (returns domain model) ↓ Router (converts domain → response via mapper) ↓ HTTP Response (Pydantic model) ``` **Benefits:** - Domain logic evolves without affecting API shape - Services are reusable across different frontends (GraphQL, gRPC, CLI) - Testing is simpler (no Pydantic overhead) - Changes to endpoint responses don't require service changes --- ## Existing Domain Models | Domain | Domain Model(s) | Mapper Module | |--------|----------------|---------------| | **Ban** | `DomainActiveBanList`, `DomainActiveBan`, `DomainBansByCountry` | `ban_mappers.py` | | **Jail** | `DomainJailList`, `DomainJailDetail`, `DomainJailBannedIps`, `DomainActiveBan` | `jail_mappers.py` | | **Config** | `DomainJailConfig`, `DomainJailConfigList`, `DomainGlobalConfig`, `DomainServiceStatus`, `DomainBantimeEscalation`, `DomainFilterConfig`, `DomainFilterList`, `DomainRegexTest`, `DomainMapColorThresholds` | `config_mappers.py` | | **History** | `DomainHistoryList`, `DomainHistoryBanItem`, `DomainIpDetail`, `DomainIpTimelineEvent` | `history_mappers.py` | | **Server** | `DomainServerSettings`, `DomainServerSettingsResult` | `server_mappers.py` | | **Blocklist** | `DomainBlocklistSource`, `DomainImportLogEntry`, `DomainImportLogList`, `DomainImportSourceResult`, `DomainImportRunResult`, `DomainPreviewResult`, `DomainScheduleConfig`, `DomainScheduleInfo` | `blocklist_mappers.py` | --- ## The Pattern — Step by Step ### Step 1: Define Domain Model in `app/models/{domain}_domain.py` ```python from dataclasses import dataclass @dataclass(frozen=True) class DomainJailConfig: """Configuration snapshot of a single jail (domain model).""" name: str ban_time: int max_retry: int find_time: int fail_regex: list[str] actions: list[str] # ← no default BEFORE default = FIELD ORDER ERROR date_pattern: str | None = None # ← all fields with defaults come AFTER log_encoding: LogEncoding = "UTF-8" ``` **⚠️ Field Order Rule:** All fields without defaults must appear before all fields with defaults. ### Step 2: Add Mapper in `app/mappers/{domain}_mappers.py` ```python def map_domain_jail_config_to_response(domain: DomainJailConfig) -> JailConfig: """Convert domain jail config to response model.""" return JailConfig( name=domain.name, ban_time=domain.ban_time, ... ) ``` ### Step 3: Service Returns Domain Model ```python # In app/services/jail_service.py from app.models.config_domain import DomainJailConfig, DomainJailConfigList async def get_jail_config(socket_path: str, name: str) -> DomainJailConfig: ... return DomainJailConfig(...) # ← return domain model ``` ### Step 4: Router Uses Mapper at Boundary ```python # In app/routers/jail_config.py from app.mappers import config_mappers @router.get("/{name}", response_model=JailConfigResponse) async def get_jail_config(...) -> JailConfigResponse: domain_result = await config_service.get_jail_config(socket_path, name) return config_mappers.map_domain_jail_config_to_response(domain_result) ``` --- ## Reference Implementation `ban_service.py` + `ban_mappers.py` is the canonical example of the correct pattern. Study it first when adding a new service. --- ## Common Issues ### Field Ordering Error ``` TypeError: non-default argument 'actions' follows default argument ``` **Fix:** Move all fields with defaults (`field: T | None = None`) after all fields without defaults. ### Forgetting the Mapper If you refactor a service to return a domain model but forget to update the router, you'll get a type mismatch at the boundary. Always update router + service together.