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>
5.0 KiB
Advanced — Multi-tenancy
Capabilities: MongoCapability.SHARED_COLLECTION_TENANCY (Advanced),
MongoCapability.DATABASE_PER_TENANT (Experimental)
Properties: ca-skeleton.persistence-mongo.advanced.shared-collection-tenancy.enabled,
ca-skeleton.persistence-mongo.advanced.database-per-tenant.enabled
1. Shared collection
Every tenant's documents live in one collection, discriminated by a tenant field.
MongoTenantContext carries the tenant. MongoTenantPredicateInjector adds the tenant predicate to
every query, every atomic filter and the first aggregation stage. TenantScopedMongoOperations
is the entry point, so a caller cannot construct an unscoped operation by forgetting.
Three details are load-bearing:
- Injection, not convention. A tenant predicate that each query is expected to add itself is a
cross-tenant leak waiting for one missed
where(...). The injector adds it structurally. - First aggregation stage.
firstStageMatch(...)places the tenant$matchbefore anything else. A$lookupor$groupthat runs before the tenant filter has already crossed the boundary, even if a later stage filters the output. - An absent tenant is not "all tenants". The injector takes an
Optional<MongoTenantContext>so the missing case is a decision the policy makes explicitly, not a predicate that quietly disappears.
MongoTenantManifestValidator.validate(manifest, tenantScopedUniqueIndexes) checks that every unique
index that should be per-tenant actually includes the tenant field. A unique index on email alone in
a shared collection makes an email globally unique across tenants — tenant B cannot register an
address tenant A already used, which is both a bug and an information leak.
requireShardKeyAnalysed(...) requires a shard-key readiness report before a shared-collection tenant
model is sharded: tenant id as a shard key prefix concentrates the largest tenant on one shard.
Observability
tenantId and rawTenantId are on MongoObservationConvention's forbidden tag list. Cardinality
grows with the customer list, and the tag ships tenant identity into the metrics backend.
2. Database per tenant (Experimental)
MongoTenantDatabaseResolver maps a tenant to its database; MongoTenantClientRegistry holds the
clients.
Experimental for a specific reason: it is correct in the small and unbounded in the large. Each tenant database costs connections, file handles and monitoring cardinality. It works beautifully at 20 tenants and falls over at 2,000, and nothing in a functional test distinguishes the two. Promotion requires operational scale evidence.
MongoTenantLifecyclePolicy:
requireActivationReady(tenantKey, schemaAndIndexesValidated)— a tenant is not activated until its schema and indexes are validated. Activating first means the first customer request is the migration test.requireDeleteAllowed(...)— deletion requires an explicit retention decision. Dropping a tenant database is irreversible and takes the backup surface with it.
MongoTenantMigrationCoordinator runs a migration across tenant databases with per-tenant results.
Partial failure is normal and must be reported per tenant: "migration failed" across 500 databases is
not a report anyone can act on.
3. Choosing
| Shared collection | Database per tenant | |
|---|---|---|
| Isolation | Logical, enforced by injection | Physical |
| Tenant count | Unbounded | Bounded by connections and file handles |
| Per-tenant restore | Hard | Natural |
| Noisy neighbour | Shared resources | Isolated |
| Migration | One collection | N databases, partial failures |
| Cross-tenant query | Possible (and must be forbidden) | Structurally impossible |
Shared collection is the default. Database-per-tenant is for a small number of tenants with a contractual isolation or per-tenant-restore requirement.
4. Failure recovery
| Symptom | Cause | Action |
|---|---|---|
| Cross-tenant data visible | An operation bypassed TenantScopedMongoOperations |
Treat as a security incident. Find the path, close it, audit access. |
| Unique constraint fires across tenants | Unique index missing the tenant field | Rebuild the index with the tenant field as prefix; the validator catches this before it ships. |
| One shard holds most data | Tenant id as shard-key prefix with a dominant tenant | Refine the shard key with a high-cardinality suffix. |
| Connection exhaustion | Database-per-tenant beyond the connection budget | The scale limit. Consolidate or move to shared collections. |
| Migration partially applied across tenants | Normal | MongoTenantMigrationCoordinator reports per tenant; re-run for the failures only. |
5. Promotion evidence
Shared-collection tenancy: actual-topology evidence, a security review covering cross-tenant access, a migration path, failure cases (injection proven on query, atomic filter and first aggregation stage), this runbook. Database-per-tenant additionally requires operational scale evidence and stays Experimental until it exists.