Files
clean-architecture-backend-…/docs/adr/ADR-MONGO-004-index-schema-admin-plane.md
T
DongHyeonkaandClaude Opus 5 d57d2f62a0 feat(mongodb): implement the MongoDB document persistence platform
Implements the mongodb-superpowers-package design: Stable Tasks 1-50 and
Advanced Tasks 1-15.

The design assumes 19 Stable + 12 Advanced Gradle projects under
modules/mongodb*. This repository's fail-closed registry declares exactly 19
leaf identities, so those modules become package boundaries inside the
registered leaf :adapter:outbound:persistence-mongo, with the design's module
dependency table enforced by ten ArchUnit rules. The mapping and every
deviation are recorded in docs/mongodb/repository-adaptation.md.

Contract highlights, all enforced by tests rather than convention:

- Transaction body retry and commit retry are separate loops. A new session per
  body attempt; commit-only retry on an unknown commit. The body is never
  replayed after a commit ambiguity, so a failover cannot become a duplicate.
- MongoExecutionOutcome keeps both ambiguous outcomes distinct from success and
  failure, and MongoFailureContext records only the design-permitted fields.
- Failure classification reads server error labels before numeric codes.
- BSON representations come from a pinned manifest, never a library default,
  and a golden type-signature gate fails on any drift.
- Index and validator changes go through the manifest and the admin plane;
  metadata ownership gates every drop.
- Every Advanced capability refuses construction unless its flag is enabled.

Verified against real servers, not only unit tests. Running the lanes for the
first time exposed four defects that a green `check` had hidden:

- Four release lanes passed while executing zero tests; the gate now counts
  executed tests per lane and fails on zero.
- The "single replica set" fixture was a standalone, because Testcontainers 2.x
  needs withReplicaSet(); its test only asserted a connection string.
- The three-node fixture was three independent clusters, so no election could
  occur, and awaitNewPrimary() compared against the post-stop primary.
- The migration lease checked modifiedCount, so a same-millisecond refresh read
  as a lost lease.

scripts/verify-mongodb-platform.sh now reports:
  9 lanes, 0 skipped, 0 failed, every evidence category produced.

scripts/verify-mongodb-advanced.sh reports NOT PROMOTABLE: actual-topology
evidence (real sharded cluster, real KMS, real target deployment) is
unobtainable here, so it is named rather than assumed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 13:41:00 +09:00

3.7 KiB

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.