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>
4.5 KiB
BSON Mapping Guide
Design §10 and decision D-06. The representation of a value in BSON is a data contract, not an
implementation detail: once a collection holds a million documents, changing how a BigDecimal is
stored is a migration with downtime, not a code change. MongoTypeRepresentationManifest pins the
representation so a library upgrade or a different default cannot move it.
1. The manifest
MongoTypeRepresentationManifest.standard() fixes:
| Java type | BSON | Representation type |
|---|---|---|
UUID |
Binary subtype 4 |
MongoUuidRepresentation.STANDARD |
BigDecimal |
Decimal128 |
MongoDecimalRepresentation.DECIMAL_128 |
BigInteger |
Decimal128 (or String when out of range, declared) |
MongoBigIntegerRepresentation |
Instant / OffsetDateTime / ZonedDateTime |
UTC Date |
MongoTemporalRepresentation.UTC_DATE |
LocalDate |
String (ISO-8601) or UTC Date, declared per field |
MongoTemporalRepresentation |
enum |
String name |
MongoEnumRepresentation.NAME |
MongoMappingConfiguration and MongoCustomConversionsFactory build the Spring Data converters from
the manifest, so there is one place to read and one place to change.
2. UUID
UuidRepresentation.STANDARD (subtype 4), always. The driver's legacy Java representation
(subtype 3) byte-swaps two halves of the UUID, so a document written by one representation and read
by the other yields a different — and valid-looking — UUID. Nothing errors; you just get the wrong
row. The golden snapshot kit pins the codec explicitly for this reason
(MongoBsonSnapshot.defaultRegistry()).
3. Decimal
BigDecimal → Decimal128, never Double. 12.30 stored as a double is 12.299999999999999, and
a monetary comparison written against it will one day be wrong by a cent for a customer who notices.
BigDecimalToDecimal128Converter / Decimal128ToBigDecimalConverter are registered from the
manifest.
Decimal128 has 34 significant digits; a BigDecimal beyond that range fails on write rather than
rounding silently.
4. Time
Store instants, not local times. LocalDateTimeMappingGuard refuses LocalDateTime fields on a
mapped document: a LocalDateTime has no offset, so the value that goes in depends on the JVM
default zone of whichever instance wrote it, and the two instances in a rolling deploy can disagree.
Use Instant when the moment matters and LocalDate when the calendar day matters.
5. Type metadata
MongoTypeMetadataPolicy decides what goes in _class:
| Policy | Stored | Use when |
|---|---|---|
NONE |
nothing | The collection holds exactly one type and never will hold a subtype. |
ALIAS |
a registered short alias | A polymorphic hierarchy in a long-lived collection. |
CLASS_NAME |
the FQCN | Short-lived or internal collections only. |
PolicyAwareMongoTypeMapper enforces it, and MongoTypeMetadataRegistry holds alias → class.
A @LongLivedMongoDocument type with CLASS_NAME is refused: writing com.example.OrderV2 into a
million documents means that renaming the package is a data migration.
6. Missing versus null
The golden kit keeps these apart deliberately. MongoBsonSnapshotAssert.hasNoField(...) and
hasExplicitNull(...) are different assertions, because in MongoDB they are different documents:
{"a": null} matches {a: null} and {a: {$exists: true}}, while {} matches only the first.
A mapper change that starts writing explicit nulls silently changes what your queries return.
7. Golden representation tests
Every collection with a fixed representation should have a snapshot test:
MongoBsonSnapshot snapshot = MongoBsonSnapshot.of(storedDocument);
MongoBsonSnapshotAssert.assertThat(snapshot)
.hasBsonType("amount", "DECIMAL128")
.hasBsonType("externalId", "BINARY")
.hasNoJavaClassName("dev.caskeleton")
.hasTypeSignature("_id:OBJECT_ID,amount:DECIMAL128,createdAt:DATE_TIME,externalId:BINARY");
hasTypeSignature is the regression gate: it fails on any representation change, including ones a
value-equality assertion would pass. When it fails, the question is whether the change was intended
and has a migration — not whether to update the string.
8. Round trips
MongoRoundTripContract asserts that write → read returns an equal domain object and that
write → read → write produces an identical BSON document. The second half is what catches an
asymmetric converter: a value that reads back equal but re-serialises differently makes every
subsequent save() a spurious update, and turns change streams into a noise generator.