# 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 `$match` before anything else. A `$lookup` or `$group` that 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` 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.