# Schema Design — Derived Rules > Derives from `domains/data/first-principles.md` P1 (Truth), P3 (Invariants in Schema), P7 (Type Fidelity). ## The Schema Reflects the Domain (P1 Truth) - A `users` table has columns that are attributes of a user, not attributes of the application. - If a column is named `is_active_for_feature_X`, the schema is lying. The domain does not have "feature X." - Normalize until it hurts, then denormalize only with evidence (P10 Performance Awareness). ## Invariants in the Schema (P3) - NOT NULL where the value is required. UNIQUE where the value is unique. - CHECK constraints for range/domain: `age >= 0`, `status IN ('draft', 'published')`. - FOREIGN KEY for relationships. The database enforces; the application defends. - A constraint in the application but not the schema is a constraint that can be bypassed. ## Types (P7 Type Fidelity) - `UUID` for IDs, not `VARCHAR`. `UUID` is a type; `VARCHAR(36)` is a string that looks like a UUID. - `TIMESTAMPTZ` for timestamps, not `VARCHAR` or `INTEGER`. Timezone-aware by default. - `ENUM` for finite domains, `VARCHAR` with CHECK for evolving domains. - `JSONB` for unstructured/semi-structured; not for data that should be a column. - `DECIMAL`/`NUMERIC` for money, never `FLOAT`. Floating point is for measurements, not money. ## Naming (P6 Naming Consistency) - snake_case for tables and columns (PostgreSQL convention): `user_accounts`, `created_at`. - Singular table names (`user` not `users`) OR plural (`users` not `user`) — pick one, be consistent. - Foreign keys: `_id` (`user_id`), not `uid` or `user`. - Junction tables: alphabetical (`order_products`, not `products_orders`). ## Avoid (P2 Normalization Discipline) - Computed columns that duplicate derivable data. Use a view or compute on read. - `created_by_name` (denormalized) when `created_by_id` + JOIN suffices. Denormalize only with evidence. - Soft-delete columns (`is_deleted`) without a corresponding constraint/behavior. Soft delete is a lifecycle decision (P8). ## Soft Delete vs Hard Delete (P8 Lifecycle Awareness) - Soft delete (`deleted_at TIMESTAMP`) preserves auditability but complicates every query. - Hard delete loses history. Choose based on the domain's legal/audit requirements. - If soft delete: every query filters `WHERE deleted_at IS NULL` by default. A missing filter is a bug. ## What Violates Schema Design | Violation | Principle | |-----------|-----------| | `VARCHAR` for a UUID | P7 Type Fidelity | | No FOREIGN KEY on a relationship | P3, P9 Referential Integrity | | `FLOAT` for money | P7, P1 Truth | | `is_deleted` without consistent filtering | P8 Lifecycle | | A column named after a feature, not a domain concept | P1 Truth |