10 KiB
adapter:outbound:persistence-mongo — module rules
Registered identity
- Module ID:
adapter-outbound-persistence-mongo - Gradle path:
:adapter:outbound:persistence-mongo - Focused test (derived from Gradle path):
./gradlew :adapter:outbound:persistence-mongo:test --console=plain - Runtime baseline: Java 21; repository framework baseline: Spring Boot 4.0.0.
- Registry SSOT:
src/config/architecture/modules.json.
Package root: dev.caskeleton.adapter.outbound.mongo. Driven (outbound) adapter — opt-in MongoDB
Document Persistence Platform. Design rationale lives in README.md; the mapping from the
design package's assumed module layout onto this leaf lives in
docs/mongodb/repository-adaptation.md and is the file to update when that mapping changes.
Responsibility
- Provide the opt-in Mongo client, template and platform policy surface without shipping a fake business domain. Real forks add their own document, repository, mapper, and application/domain port implementation.
- It does not reimplement idempotency / outbox / lock on Mongo (those stay JPA-only).
- Opt-in:
MongoPersistenceConfigre-imports the Mongo auto-configuration (@ImportAutoConfiguration) only whenca-skeleton.persistence-mongo.enabled=true(default off).MongoPlatformAutoConfigurationis gated on the same flag. The connection URI and database come from Spring's standardspring.data.mongodb.*settings; platform profiles come fromca-skeleton.persistence-mongo.platform.*. MongoOptInAutoConfigurationImportFilter, registered throughMETA-INF/spring.factories, blocks Boot 4's classpath-driven sync/reactive/data/repository/health/metrics Mongo auto-configuration when the module enable flag is absent or false.
Allowed
- No project dependency at all. The registry entry's
allowed_dependenciesis[], matching what the build actually uses;domain-core,application-coreandshared-contractwere listed and unused, which is a permission granted in advance for an adapter nobody has approved yet.verifyCleanArchitectureDependenciesonly checks that resolved edges are a subset of the declared ones, so an unused permission passes every run;MongoRegistryPermissionParityTestchecks the other direction and fails when the two sets differ. - The composition root declares
implementation(project(':adapter:outbound:persistence-mongo'))— with the reactive starter and the reactivestreams driver excluded, because there is no reactive port in the shipped Stable scope. The actual Gradle runtime graph is the runtime SSOT; runtime classpath, so the membership cannot drift from what the jar carries. Property-only activation therefore works here: the switch turns on a module that already ships, and shipping it off is not the same contract as leaving it out, because absence cannot be reversed at deploy time and hides every gating defect.sample-portfoliodoes not carry it. - External:
spring-boot-starter-data-mongodband-reactive,spring-boot-autoconfigure,micrometer-core,slf4j-api,spring-boot-configuration-processor(annotation processor). Versions come from the shared Spring Boot BOM; never pin the driver directly. - Test-only: ArchUnit, reactor-test, Testcontainers (
mongodb,toxiproxy).
Forbidden
- Inbound adapters, sibling outbound adapters,
app-bootstrap,sample-portfolio(ArchUnitOUTBOUND_ADAPTERS_*family rules). - Shipping placeholder
Example*document, repository, record, or adapter types in production. - Adding idempotency/outbox/lock on Mongo without a separately approved contract.
- Fully-qualified inline type references; more than one public top-level type per file.
Package-boundary rules (MongoModuleBoundaryTest)
These reproduce the design's module dependency table. Breaking one fails the build:
…mongo.api..must not import Spring, the MongoDB driver, BSON or Reactor. It is the framework-free core contract;api/package-info.javarecords why.- No Stable package may depend on
…mongo.advanced... - No production package may depend on
…mongo.testkit... imperative↛reactive,query↛aggregation,schema↛ execution packages,observation↛ execution packages,migration↛migration.flamingock.
Consistency-bound templates
MongoConsistencyBinder derives one MongoTemplate per consistency profile. Spring Data exposes
setters and no getters for a template's supporting contract, so the parts that must travel —
write-concern resolver, write-result checking, lifecycle events, and the ApplicationContext that
supplies entity callbacks and auditing — are passed explicitly as a MongoTemplateSupportContract.
Reflection over private fields is forbidden here. A template configured with callbacks registered
programmatically outside a context cannot be reproduced by the bound path; that configuration is
unsupported rather than silently dropped.
Public surface
Every public top-level type in one jar means public is public to every adopter regardless of which
package it sits in. verifyMongoApiSurface (in check) compares the surface against
docs/architecture/mongo-api-surface.txt, which carries the count; growing it takes
updateMongoApiSurface -PapproveMongoApiSurfaceChange, which is a review decision.
The architecture rule catalogue (…mongo.architecture) is in the testkit source set, not
production: it is ArchUnit input, and shipping it put rule text on every consumer's runtime
classpath. Release gating is testkit-only for the same reason and is one implementation, not two:
…mongo.testkit.release reads the JUnit XML a lane wrote and is what
scripts/verify-mongodb-platform.sh and src/config/mongodb/release-contracts.json drive. A second
pair on the production classpath — a hand-built set of category names and a gate that checked it —
had no caller outside its own test and no source of truth behind the categories; it is gone rather
than moved.
Still pending, and deliberately not done as part of a review sweep: moving implementation packages
under an internal root and lowering visibility inside them. That is a mechanical change over ~200
files and belongs in its own commit, after which the snapshot above is what proves the surface
actually shrank.
Credentials and production profiles
A production MongoProfileProperties must declare TLS, authentication, strict Stable API, a
non-standalone topology and finite positive timeouts; validate() enforces all of them. The URI is a
secret:// reference and MongoCredentialResolver is the only thing that turns one into a
connection string — it hands the value to a caller-supplied function and never returns it, so the
credential is a local for one call rather than a field, a bean property or a log line.
Not shipped: the resolver implementation (a fork wires its own secret store), and the TLS lane
(trusted CA, wrong CA, hostname mismatch, expired certificate) which needs a container with real
certificates. Rotation ordering — new generation ready, traffic switch, old lease drain, close — is
covered by MongoClientGenerationConcurrencyTest.
Change-stream lifecycle
MongoChangeStreamPipeline owns ordering: it drives events through MongoChangeStreamRunner with
concatMap and refuses any event behind its cluster-time high-water mark, so a checkpoint write
always means "everything up to here is done". Callers must feed it a Flux in stream order rather
than calling runOne themselves.
The driver-side source — watch, resumeAfter/startAfter, cursor lifetime and reconnection — is
not shipped. A fork wires its MongoChangeStreamSubscription to the driver and feeds the
pipeline; the resume checkpoint it must pass back is MongoResumeCheckpointStore's. Saying this
plainly is the point: the policy and value objects here do not add up to a running consumer.
Platform invariants that are not stylistic
- Transaction body retry and commit retry are separate loops: a new session per body attempt, and
commit-only retry on an unknown commit. The body is never replayed after a commit ambiguity
(
MongoTransactionRetryCoordinator, ADR-MONGO-003). MongoExecutionOutcome's two ambiguous values must not be collapsed into success or failure.- BSON representations come from
MongoTypeRepresentationManifest, never from a library default (ADR-MONGO-002). - Index and validator changes go through the manifest and the admin plane; ownership gates every drop (ADR-MONGO-004).
- Every concrete Advanced type is annotated
@MongoAdvancedEntryPoint(capability)or@MongoAdvancedPolicy, and an entry point takes aMongoAdvancedCapabilityGuardand callsrequirebefore it exists.MongoAdvancedRulesenforces both halves; the classification rule is what keeps "we gate the executable ones" from meaning "the ones somebody remembered". - The flags are bound from
ca-skeleton.persistence-mongo.advanced.<capability>.enabledbyMongoAdvancedProperties.MongoAdvancedConfigurationis imported by name, never auto-loaded: the Stable lane must reference nothing under..advanced... MongoSearchOperations,MongoVectorSearchOperationsandMongoTimeSeriesOperationsare scaffold — contracts with no implementation in this repository. A fork supplies the Atlas or time-series deployment and the implementation.- Observation tags are limited to
MongoObservationConvention's allowlist.
Tests
MongoPersistenceConfigTest proves default/false behavior through an actual
@EnableAutoConfiguration context, typed enablement binding, and enabled infrastructure with a
mock MongoClient plus a real MongoTemplate without a network connection. It must keep passing —
the platform additions are opt-in and must not turn the module on by existing.
Hermetic contract tests carry @Tag("mongodb-contract") and run in mongoStableContractTest, which
check depends on. Container lanes carry mongodb-replicaset / mongodb-failover and run only in
their own tasks; the default test task excludes them, because a lane that needs Docker inside
check teaches people to skip check.
cd src
./gradlew :adapter:outbound:persistence-mongo:check --console=plain
Release gates run from the repository root: scripts/verify-mongodb-platform.sh (Stable) and
scripts/verify-mongodb-advanced.sh (Advanced).