Files
clean-architecture-backend-…/src/adapter/outbound/persistence-mongo/CLAUDE.md
T

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: MongoPersistenceConfig re-imports the Mongo auto-configuration (@ImportAutoConfiguration) only when ca-skeleton.persistence-mongo.enabled=true (default off). MongoPlatformAutoConfiguration is gated on the same flag. The connection URI and database come from Spring's standard spring.data.mongodb.* settings; platform profiles come from ca-skeleton.persistence-mongo.platform.*.
  • MongoOptInAutoConfigurationImportFilter, registered through META-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_dependencies is [], matching what the build actually uses; domain-core, application-core and shared-contract were listed and unused, which is a permission granted in advance for an adapter nobody has approved yet. verifyCleanArchitectureDependencies only checks that resolved edges are a subset of the declared ones, so an unused permission passes every run; MongoRegistryPermissionParityTest checks 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-portfolio does not carry it.
  • External: spring-boot-starter-data-mongodb and -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 (ArchUnit OUTBOUND_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.java records why.
  • No Stable package may depend on …mongo.advanced...
  • No production package may depend on …mongo.testkit...
  • imperativereactive, queryaggregation, schema ↛ execution packages, observation ↛ execution packages, migrationmigration.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 a MongoAdvancedCapabilityGuard and calls require before it exists. MongoAdvancedRules enforces 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>.enabled by MongoAdvancedProperties. MongoAdvancedConfiguration is imported by name, never auto-loaded: the Stable lane must reference nothing under ..advanced...
  • MongoSearchOperations, MongoVectorSearchOperations and MongoTimeSeriesOperations are 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).