Files
clean-architecture-backend-…/docs/mongodb/repository-adaptation.md
T

11 KiB

MongoDB Document Persistence Platform — Repository Adaptation Contract

Design source: docs/superpowers/specs/2026-08-11-mongodb-document-persistence-platform-design.md Stable plan: docs/superpowers/plans/2026-08-11-mongodb-document-persistence-platform-implementation-plan.md Advanced plan: docs/superpowers/plans/2026-08-11-mongodb-advanced-capabilities-expansion-plan.md

The design package declares its own module root (modules/mongodb) and root package (io.backend.skeleton.mongodb) as implementation assumptions, not as contract. This file is the single record of how that assumed layout was mapped onto this repository. Only paths, build DSL, and composition-root ownership changed. Public contracts, policy order, and error semantics are implemented exactly as specified.

1. Why the module layout differs

The design assumes 19 Stable Gradle projects under modules/mongodb/ and 12 Advanced projects under modules/mongodb-advanced/. This repository is a Clean Architecture template whose fail-closed registry (src/config/architecture/modules.json, enforced by src/settings.gradle and verifyCleanArchitectureDependencies) declares exactly 19 leaf identities. Creating 31 more Gradle projects would violate HARD-STOP #5 in AGENTS.md.

Therefore the design's 31 modules become package boundaries inside the registered leaf :adapter:outbound:persistence-mongo, following the precedent already set by docs/httpclient/repository-adaptation.md. The design's module dependency table (§6.3) is enforced by MongoModuleBoundaryTest as a closed edge matrix: every top-level package is declared with the packages it may import, the matrix is compared against the tree for exact equality, and every observed edge must appear in it. A forbidden edge fails the build the same way a missing Gradle dependency would, and so does a new package nobody registered.

This used to be a stronger claim than the test. The rules forbade a handful of reverse dependencies and said nothing about the rest, so four edges outside the design's DAG existed and passed: reactive → imperative, reactive → query, transaction → reactive and geo → imperative. They are declared in the matrix now rather than removed — each is a real coupling the code relies on, and the point of recording them is that the next one is a decision instead of an accident.

2. Package mapping

Root package: io.backend.skeleton.mongodbdev.caskeleton.adapter.outbound.mongo.

2.1 Stable modules

Design module Repository package
mongodb-core-api …outbound.mongo.api (+ .capability, .consistency, .error, .mapping, .observation, .profile, .schema)
mongodb-spring-data …outbound.mongo.mapping (+ .type), …outbound.mongo.failure
mongodb-imperative …outbound.mongo.imperative (+ .atomic, .bulk, .revision)
mongodb-reactive …outbound.mongo.reactive (+ .cursor)
mongodb-query …outbound.mongo.query (+ .budget, .pagination)
mongodb-aggregation …outbound.mongo.aggregation
mongodb-transaction …outbound.mongo.transaction (+ .retry, .session)
mongodb-index-schema …outbound.mongo.schema (+ .index, .manifest, .model, .ttl, .validation)
mongodb-change-stream …outbound.mongo.changestream (+ .projector, .recovery)
mongodb-geospatial …outbound.mongo.geo
mongodb-migration-core …outbound.mongo.migration
mongodb-migration-flamingock …outbound.mongo.migration.flamingock
mongodb-observability …outbound.mongo.observation
mongodb-security …outbound.mongo.security (+ .admin), …outbound.mongo.nativecap
mongodb-spring-boot-starter …outbound.mongo.autoconfigure
mongodb-testkit-core …outbound.mongo.testkit.mapping, .compat, .performance (testkit source set)
mongodb-testkit-replicaset …outbound.mongo.testkit.rs (testkit source set)
mongodb-testkit-failover …outbound.mongo.testkit.failover (testkit source set)
mongodb-testkit-migration …outbound.mongo.testkit.migration (testkit source set)

…outbound.mongo.architecture has no design counterpart: it holds the @MongoOperation marker and the reusable ArchUnit rule set a fork applies to its own document/repository code.

2.2 Advanced modules

Design module Repository package
mongodb-sharding …outbound.mongo.advanced.sharding (+ .admin for the D4 shard plane)
mongodb-timeseries …outbound.mongo.advanced.timeseries
mongodb-csfle …outbound.mongo.advanced.encryption.csfle
mongodb-queryable-encryption …outbound.mongo.advanced.encryption.qe
mongodb-search …outbound.mongo.advanced.search
mongodb-vector-search …outbound.mongo.advanced.vector
mongodb-tenancy-shared …outbound.mongo.advanced.tenancy.shared
mongodb-tenancy-database …outbound.mongo.advanced.tenancy.database
mongodb-change-stream-messaging-bridge …outbound.mongo.advanced.bridge
mongodb-gridfs-compat …outbound.mongo.advanced.gridfs
mongodb-testkit-sharded …outbound.mongo.testkit.sharded (testkit source set)
mongodb-testkit-atlas …outbound.mongo.testkit.atlas (testkit source set)

The design's rule that a Stable module never depends on an Advanced one survives as an ArchUnit rule (stableNeverDependsOnAdvanced) plus the opt-in flag: every Advanced entry point requires MongoAdvancedCapabilityFlags to have the matching capability enabled and refuses construction otherwise. Being on the classpath is not being enabled.

3. Other deliberate substitutions

Design assumption Repository reality Adaptation
Gradle Kotlin DSL under modules/mongodb* Groovy DSL, root build.gradle conventions, LockMode.STRICT locking Dependencies declared in src/adapter/outbound/persistence-mongo/build.gradle; gradle.lockfile regenerated.
mongodb-spring-boot-starter is a separate module the app depends on modules.json gives adapter-outbound-persistence-mongo runtime_memberships: [] and does not list it among app-bootstrap's allowed dependencies The autoconfigure package stays inside the leaf and registers through the leaf's own META-INF/spring/…AutoConfiguration.imports. This differs from the httpclient precedent, where the starter moved to :app-bootstrap; here the registry forbids that edge.
Spring Boot 4.1 / Spring Data MongoDB 5.1 baseline Repository baseline is Spring Boot 4.0.0 / Spring Data MongoDB 5.0.0 The platform targets the Spring Data MongoDB API surface common to both; no 5.1-only type is referenced. The support matrix records the actual pinned versions.
MongoRetryScope lives in mongodb-transaction The mongodb-spring-data failure translator must classify retry scope, and it cannot depend on mongodb-transaction MongoRetryScope lives in …api.error (core-api), which both packages already depend on. Same values, same meaning, one legal position in the DAG.
mongodb-migration-flamingock depends on Flamingock Adding an unvetted external dependency is out of scope for this task, and the design itself requires the public contract not to depend on Flamingock types The adapter is provider-neutral: it consumes a platform-owned FlamingockChangeUnitView. Wiring an actual Flamingock distribution is a one-file change behind that view.
Testkit as its own Gradle module The design forbids production modules depending on the testkit A dedicated testkit source set whose output is on the test compile/runtime classpaths only. ArchUnit rule productionNeverDependsOnTestkit enforces the direction.
Per-task git commit AGENTS.md: commit policy is human-only Implementation is delivered unstaged; commits are the human's action. This is the only plan step intentionally not executed, and it is recorded here.
docs/mongodb/**, scripts/verify-mongodb-*.sh Repository already owns docs/ and scripts/ Created at the same repository-relative paths. The two gate scripts were later removed (2026-08-15); see §5.

4. What is unchanged from the design

  • D1 / D2 / D3 / D4 exposure planes and the ordered D3 admission sequence (§5).
  • Stable API V1 with apiStrict=true on the D1/D2 client generation; D3/D4 on separate generations.
  • MongoExecutionOutcome, including both ambiguous outcomes (WRITE_RESULT_UNKNOWN, TRANSACTION_COMMIT_UNKNOWN), and MongoFailureContext's permitted-field list.
  • The complete stable exception hierarchy and the label-before-code classification order.
  • The BSON representation manifest (UUID STANDARD, Decimal128, UTC instants, alias type metadata) and the document-size budget.
  • Update-operator-first writes, and optimistic revision as the precondition for whole-document replacement.
  • Transaction body retry and commit retry as separate loops: a new session per body attempt, and commit-only retry on unknown commit. The body is never replayed after a commit ambiguity.
  • Registered operation names and manifests for query, aggregation and index; no free-form JSON query and no unbounded pipeline.
  • Keyset pagination with an authenticated cursor and a unique tie-breaker requirement.
  • Change stream as an at-least-once projector with resume-token checkpointing and explicit history-lost handling.
  • TTL as physical cleanup only, never the sole basis for access denial or business scheduling.
  • Manifest-owned index/validator state with an apply policy that never drops what it does not own.
  • Low-cardinality observation tags, command redaction, and the credential reference indirection.
  • The Stable release gate's evidence categories, and the Advanced promotion gate's requirement for actual-topology evidence.

5. Verification

The two release-gate scripts (scripts/verify-mongodb-platform.sh and scripts/verify-mongodb-advanced.sh) were removed on 2026-08-15. They wrapped the Gradle lanes below and added two things Gradle does not do on its own: a lane that executed zero tests was reported as a failure rather than counted as a pass, and a promotion.json recording the commit, server image and contract-manifest hash. Neither exists until something replaces it, so a green run of the commands below is weaker evidence than the gate was.

From src/:

./gradlew :adapter:outbound:persistence-mongo:check --console=plain
./gradlew verifyCleanArchitectureDependencies --console=plain
./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest' --console=plain

The container-backed lanes the gate ran behind MONGODB_DOCKER=1mongoCompatibilityTest, mongoMigrationTest, mongoSecurityIntegrationTest, mongoReplicaSetTest, mongoFailoverTest, mongoPerformanceTest — are now invoked by name or not at all.