docs(P03): complete domain-derived-docs phase

This commit is contained in:
Jon Chery
2026-08-05 00:30:31 +00:00
parent 2c15282ad4
commit 530ce3efed
27 changed files with 1556 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
# Indexing — Derived Rules
> Derives from `domains/data/first-principles.md` P5 (Indexing with Intent), P10 (Performance Awareness).
## Index for Queries, Not Tables (P5)
- An index serves a query. No query, no index.
- The query plan is the spec. `EXPLAIN` is the test. An index that is not used is dead weight.
- Index the columns you filter on (`WHERE`), join on (`JOIN`), and sort on (`ORDER BY`).
## Composite Indexes (P5, P10)
- Order matters: `INDEX(a, b)` serves `WHERE a = ? AND b = ?` and `WHERE a = ?`, but NOT `WHERE b = ?`.
- Put the most selective column first. Or the column used in every query. Depends on the workload.
- An index on every column is not a strategy. It is write amplification.
## Unique Indexes (P3 Invariants in Schema)
- A uniqueness constraint is a unique index. Use it for invariants: `email`, `username`.
- Unique indexes enforce; application checks defend. Both belong.
- A partial unique index: `UNIQUE(email) WHERE deleted_at IS NULL` — allows soft-deleted duplicates.
## Covering Indexes (P10)
- An index that covers all columns of a query is an "index-only scan" — no table lookup.
- PostgreSQL: `INCLUDE` clause. MySQL: all columns in the index.
- Use for hot queries. Don't cover everything; index size matters.
## Don't Over-Index (P10, core C8 Economy)
- Every index costs a write. The write budget is the index count.
- Indexes take disk and memory. A 1GB index on a 500MB table is a smell.
- Remove unused indexes. `pg_stat_user_indexes` shows usage. An unused index is debt.
## Migration and Indexes (P4 Migration Safety)
- Adding an index on a large table is expensive. Do it concurrently (`CREATE INDEX CONCURRENTLY`).
- An index migration that locks the table blocks writes. Plan for it.
- Build the index, then deploy the query that uses it. Not the reverse.
## What Violates Indexing Discipline
| Violation | Principle |
|-----------|-----------|
| Index on every column | P10, C8 Economy |
| No index on a foreign key | P10 (join performance) |
| `WHERE b = ?` with only `INDEX(a, b)` | P5 (wrong order) |
| Index created without checking the query plan | P5 (no intent) |
| `CREATE INDEX` (non-concurrent) on a 10M-row table in prod | P4 Migration Safety |
+53
View File
@@ -0,0 +1,53 @@
# Migrations — Derived Rules
> Derives from `domains/data/first-principles.md` P4 (Migration Safety), P5 (Reversibility via core C5).
## Every Change is a Migration (P4)
- No manual schema changes. No `ALTER TABLE` in a shell. Every change is a versioned migration file.
- Migrations are code: reviewed, tested, committed.
- The migration tool is the only way to change the schema (`prisma migrate`, `alembic`, `flyway`, `golang-migrate`).
## Forward and Reverse (P5 Reversibility, core C5)
- Every migration has an `up` and a `down`. The `down` reverses the `up`.
- A migration without a `down` is irreversible. Irreversible migrations are rare and flagged.
- Test the `down` in CI. A `down` that fails is a migration that cannot be rolled back.
## Expand, Migrate, Contract (P5)
For non-breaking schema changes:
1. **Expand**: add the new column/ table (nullable, no constraint). Deploy. Old code still works.
2. **Migrate**: backfill data, run the data migration. Deploy. Both old and new code work.
3. **Contract**: add constraints, remove the old column. Deploy after all code uses the new schema.
Never do all three in one migration. Each step is its own deploy.
## Avoid Destructive Changes (P4)
- Never `DROP COLUMN` in a migration that could be in use. Expand-contract first.
- Never `DROP TABLE` without confirming no code references it.
- Never `ALTER TYPE` in a way that locks the table on a large dataset. Use a phased approach.
## Backward Compatibility (P5, P1)
- A migration must not break the running code. Old code reads the new schema (with expand).
- The schema is always compatible with the previous code version. Two-version compatibility.
- A breaking migration is deployed in lockstep with the code, with a maintenance window.
## Testing Migrations (P3 Determinism via testing P3)
- Run migrations on a copy of production data in CI. A migration that works on dev may fail on prod scale.
- Test the `down` on the migrated state, not just the `up`.
- Test with the largest table sizes you have. `ALTER TABLE` on 10 rows is fast; on 10M rows, it may lock.
## What Violates Migration Safety
| Violation | Principle |
|-----------|-----------|
| Manual `ALTER TABLE` in prod | P4 |
| Migration with no `down` | P5 Reversibility |
| `DROP COLUMN` in the same deploy as the new code | P4, P5 |
| No migration test on prod-scale data | P3 Determinism |
| A migration that locks a table for 10 minutes | P4 (downtime) |
+53
View File
@@ -0,0 +1,53 @@
# 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 |