# ADR-MONGO-004 — Index and schema changes belong to the admin plane - **Status:** Accepted - **Date:** 2026-08-13 - **Design source:** design §21–§25, decisions D-11, D-13 ## Context Spring Data can create indexes automatically from annotations. On a laptop this is convenient. On a collection with a hundred million documents, an index build is a capacity event: it consumes CPU, IO and memory on the primary for minutes to hours, and it starts because a pod restarted. Worse, it starts N times when N pods restart, and there is no approval step, no ordering relative to the code that needs the index, and no record afterwards of what was created. Schema validators have the same shape with a sharper edge: tightening a validator on a collection with existing data rejects writes to documents that were legal when they were written. TTL has a third shape. It looks like a scheduler and is not one: the TTL monitor runs about once a minute and deletes in batches, so an expired document routinely remains readable for minutes or hours. ## Decision **Indexes and validators are declared in a manifest and applied by the admin plane (D4).** Automatic index creation in production is disabled. 1. `MongoManifestRegistry` holds the declared indexes (`MongoIndexManifest`) and validator (`MongoSchemaManifest`) per collection. The manifest is the source of truth, reviewed in a pull request. 2. `MongoIndexDiffEngine` compares manifest against observed state and reports missing, extra and *changed* indexes. Changed ones are reported rather than re-issued: MongoDB will not silently rebuild an index whose definition moved. 3. `MongoIndexApplyPolicy` sets what an environment may do — `APPLY` (local), `APPLY_WITH_DIFF` (staging), `DIFF_WITH_APPROVED_APPLY` (production), `REPORT_ONLY` (audit). 4. **Ownership gates every drop.** `MongoMetadataOwnership` distinguishes `APPLICATION_MANAGED` from `SEARCH_MANAGED`, `ENCRYPTION_MANAGED` and `EXTERNAL`. Only application-managed objects are droppable on drift. A diff engine without ownership eventually proposes dropping `enxcol_.customers.esc`, and "the drift tool cleaned it up" is a very bad incident summary. 5. **Retirement is staged.** `MongoIndexRetirementState` moves an index declared → hidden → observed-unused → droppable, one deployment per transition. Hiding is instantly reversible; dropping is a rebuild. 6. **Stable validation actions are `error` and `warn` only.** `errorAndLog` is not part of the Stable contract on 7.0 or 8.0 and is refused. Tightening goes `warn`+`MODERATE` → confirm zero warnings → `error`+`STRICT`, in two deployments. 7. **TTL is physical cleanup only** (D-13). `MongoExpirationAccessPolicy` states the rule: a document's presence is not authorization and its absence is not a deadline. Access control checks the expiry field; scheduling uses a scheduler. 8. **Migrations are checksummed, locked, precondition-checked and resumable.** `MongoMigrationRunner` fails hard when an applied id's checksum changed — two environments running different code under one id is worse than a failed deploy. ## Consequences **Positive.** Index builds are scheduled by people who know the capacity. Rollback is possible at every step. Drift is visible without being dangerous. Nothing drops what it does not own. **Negative.** Adding an index is a manifest change plus an apply, not an annotation. Local development uses `APPLY` so the friction is confined to environments where it is warranted. **Rejected alternative — "auto-create with a feature flag."** The flag is either on in production, which is the problem, or off, in which case the manifest is the real mechanism and the annotation is a second, divergent source of truth.