Files
atelier/domains/data/schema-design.md
T
2026-08-05 00:30:31 +00:00

2.7 KiB

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: <singular_table>_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