2.7 KiB
2.7 KiB
Schema Design — Derived Rules
Derives from
domains/data/first-principles.mdP1 (Truth), P3 (Invariants in Schema), P7 (Type Fidelity).
The Schema Reflects the Domain (P1 Truth)
- A
userstable 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)
UUIDfor IDs, notVARCHAR.UUIDis a type;VARCHAR(36)is a string that looks like a UUID.TIMESTAMPTZfor timestamps, notVARCHARorINTEGER. Timezone-aware by default.ENUMfor finite domains,VARCHARwith CHECK for evolving domains.JSONBfor unstructured/semi-structured; not for data that should be a column.DECIMAL/NUMERICfor money, neverFLOAT. 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 (
usernotusers) OR plural (usersnotuser) — pick one, be consistent. - Foreign keys:
<singular_table>_id(user_id), notuidoruser. - Junction tables: alphabetical (
order_products, notproducts_orders).
Avoid (P2 Normalization Discipline)
- Computed columns that duplicate derivable data. Use a view or compute on read.
created_by_name(denormalized) whencreated_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 NULLby 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 |