Files
clean-architecture-backend-…/docs/superpowers/specs/2026-07-25-module-gradle-hygiene-design.md
T

390 lines
21 KiB
Markdown

# Module and Gradle Hygiene Refactoring Design
- **Date:** 2026-07-25
- **Status:** Approved
- **Scope:** all 19 Gradle leaf modules, their project/external dependencies, test conventions,
architecture-analysis classpath, runtime composition, and dependency locks
- **Source:** repository audit performed on 2026-07-25 against commit `821fe00`
## 1. Prerequisite
CI recovery is a hard prerequisite, not part of this refactoring. The implementation may start only
after the repository again contains the harness registry and CI contract assets and these commands
reach task execution:
```bash
cd src
./gradlew projects --console=plain
./gradlew :app-bootstrap:test --tests \
'dev.caskeleton.bootstrap.contract.DeveloperExperienceContractTest' --console=plain
./gradlew :app-bootstrap:test --tests \
'dev.caskeleton.bootstrap.contract.SampleRemovalSmokeContractTest' --console=plain
./gradlew verifyTrivyignore --console=plain
```
At audit time `settings.gradle` fails before project configuration because
`.harness/project/modules.yaml` is absent. `.tool-versions`, `.trivyignore.yaml`, and the workflow
files read by the two contract tests are absent as well. Dependency removal must not be mixed with
that recovery because a red baseline cannot distinguish a pre-existing CI failure from a refactoring
regression.
## 2. Problem Statement
The module direction is broadly clean, but the declared Gradle graph is wider than the source graph:
many leaves declare every allowed core dependency even when they use only one contract. Pure-core
tests inherit Spring MVC from a global convention. `application-core` imports SLF4J for one outbox
use case and therefore carries the complete Spring Boot starter at compile and runtime. Several
leaves retain unused Groovy, Spock, generated-stub, UUID, or configuration-processor dependencies.
The existing central ArchUnit suite analyzes whatever happens to be on the
`app-bootstrap` test runtime classpath. Optional leaves are therefore not guaranteed to be analyzed.
The sample-isolation contract also carries a hard-coded subset of modules instead of reading the
19-leaf registry. Locking is strict, but the lock verifier is not a release-gate dependency and
non-BOM version ownership is scattered.
This design reduces the graph only after characterization, makes topology and architecture coverage
registry-driven, restores pure-core test isolation, and separates application logging intent from
the logging framework.
## 3. Evidence Classification
### 3.1 Observed facts
The following findings are deterministic observations and do not need dependency-removal debate:
1. `src/settings.gradle` cannot configure without `.harness/project/modules.yaml`.
2. There are exactly 19 leaf `build.gradle` files and 19 leaf `gradle.lockfile` files.
3. No production configuration depends on `:sample-portfolio`;
`app-bootstrap` has one test-only `sampleFixture` edge.
4. The only adapter-to-adapter project edges are:
`messaging`, `cache-redis`, `notification`, and `httpclient` to
`adapter:outbound:support`.
5. `domain-core` and `shared-contract` main source contain no Spring, JPA, Jackson, or SLF4J imports.
6. `application-core` main source contains no Spring import. Its only framework imports are SLF4J in
`PublishPendingOutboxEventsUseCase`.
7. The root build adds Spring Boot test and Spring MVC test starters to every leaf.
8. `cache-redis`, `messaging`, and `notification` have no Groovy tests although their builds apply
Groovy and add Spock.
9. `adapter:outbound:identifier` does not use `uuid-creator`.
10. `src/sample-portfolio/.jqwik-database` is a tracked Java-serialization runtime artifact.
11. `persistence-mongo` owns adapter-local `Example*` domain/document/repository/mapper types and its
repository adapter implements no application/domain port.
### 3.2 Static candidates
The following are source-reference candidates, not approved removals. Each must first pass a
leaf-specific compile/test characterization:
| Leaf | Candidate project edges |
| --- | --- |
| `adapter:inbound:graphql` | `application-core`, `domain-core` |
| `adapter:inbound:grpc` | `application-core`, `domain-core` |
| `adapter:inbound:web` | `domain-core` |
| `adapter:inbound:websocket` | `application-core`, `shared-contract` |
| `adapter:outbound:cache-redis` | `domain-core`, `application-core` |
| `adapter:outbound:httpclient` | `domain-core`, `application-core` |
| `adapter:outbound:identifier` | `domain-core` |
| `adapter:outbound:messaging` | `domain-core` |
| `adapter:outbound:notification` | `domain-core` |
| `adapter:outbound:persistence-jpa` | `domain-core` |
| `adapter:outbound:persistence-mongo` | `application-core`, `shared-contract` |
| `adapter:outbound:support` | `domain-core`, `application-core`, `shared-contract` |
The same characterization rule applies to these external candidates:
- GraphQL configuration processor and JSR-310 module.
- gRPC protobuf/stub/annotations dependencies in the no-generated-stub skeleton.
- broad `spring-boot-starter` usage in gRPC, fileserver, and objectstorage.
- explicit Flyway core where the starter already supplies the required API.
- duplicate starter/test declarations in app-bootstrap and sample-portfolio.
An allowed registry edge is permission, not a requirement to declare that edge.
## 4. Goals
1. Keep all module paths and allowed edges in `.harness/project/modules.yaml` only.
2. Make the actual project DAG the smallest graph required by source, tests, and runtime
composition.
3. Preserve the approved outbox failure-reporting refactor's removal of Spring and logging
frameworks from `application-core` compile/runtime classpaths.
4. Give `domain-core`, `application-core`, and `shared-contract` framework-free test conventions.
5. Enforce external dependency purity for core modules from resolved compile/runtime graphs.
6. Analyze every registered production leaf with the architecture suite regardless of runtime
composition.
7. Apply the Spring configuration processor exactly where main source declares
`@ConfigurationProperties`.
8. Verify strict locks in the release gate and assign one owner to every non-BOM version.
9. State which optional adapters are in the default app runtime and which are opt-in.
10. Remove generated jqwik state from source control.
11. Remove the Mongo adapter-local example domain from production without creating a duplicate
sample implementation.
## 5. Non-Goals
- No feature behavior, endpoint, persistence schema, or public contract change.
- No conversion to convention plugins, `buildSrc`, an included build, or a version catalog in this
change. Build-logic migration starts only from a green post-refactoring baseline.
- No automatic inclusion of every optional adapter in the production runtime.
- No new Mongo business port or second Mongo sample in `sample-portfolio`.
- No relocation of the shared ThreadLocal implementation in this change.
- No LLM Wiki write as part of this documentation-only design task.
## 6. Target Topology and Registry Policy
The registry remains the only topology authority. Every leaf entry must continue to own:
- stable id
- source path
- Gradle path
- role/family
- allowed project dependencies
- focused command
- nearest module guidance
Each runtime-capable leaf also receives one explicit runtime membership:
- `core`: contract/core leaf consumed by registered adapters or bootstrap.
- `app-default`: present on the default `app-bootstrap` runtime classpath.
- `opt-in`: built and architecture-analyzed but absent from the default application runtime.
- `sample-only`: used only by the sample fixture/runtime.
- `composition-root`: `app-bootstrap` or `sample-portfolio` itself.
The registry validator rejects missing membership, unknown dependency ids, duplicate Gradle paths,
production edges to `sample-portfolio`, adapter peer edges not explicitly allowed by the source
module's `allowed_dependencies`, and cycles. Gradle settings, the project-dependency verifier,
sample-isolation checks, and architecture-analysis classpath all consume this data. No Java test
keeps a copied module list.
The current default runtime membership is preserved during graph cleanup. Optional adapters do not
become runtime dependencies merely because architecture analysis needs their classes.
## 7. Approved Application Logging Boundary
The logging-boundary implementation is owned by
`docs/superpowers/specs/2026-07-25-application-outbox-failure-reporting-design.md` and its matching
implementation plan. That design is a prerequisite for dependency pruning in this plan and is not
redefined here.
The selected contract is:
- `application-core` owns `OutboxRelayFailureReportPort`;
- the port has one `report(OutboxRelayFailureReport)` method;
- the safe immutable report carries the approved FAILED/DEAD operational fields and never carries
payload or idempotency data;
- `adapter:outbound:messaging` owns the structured SLF4J reporter implementation;
- `app-bootstrap` injects the port into the manually assembled relay use case;
- application source and dependency guardrails prevent Spring and logging frameworks from returning
to `application-core`.
Module hygiene begins only after that focused plan is green. This design then verifies the resulting
application dependency purity and removes unrelated static-candidate edges; it does not introduce a
second reporting port or relocate reporter ownership.
## 8. Test Dependency Conventions
Test dependencies are role-specific:
| Role | Baseline |
| --- | --- |
| `domain-core` | JUnit Jupiter API/engine and AssertJ only when tests exist |
| `application-core` | JUnit Jupiter, AssertJ; hand-written fakes; no Spring context |
| `shared-contract` | JUnit Jupiter and AssertJ; no Spring context |
| inbound web/GraphQL/WebSocket/gRPC | transport test modules required by that protocol only |
| persistence adapters | mapping/unit baseline plus datastore Testcontainers only where vendor behavior is tested |
| other outbound adapters | JUnit/Spock selected by actual test language; fake external systems |
| `app-bootstrap` | Spring Boot context/slice support, ArchUnit, and integration-test dependencies |
| `sample-portfolio` | feature, property, slice, and integration-test dependencies owned by the sample |
The root build may supply JUnit platform launch/runtime configuration, but it must not supply Spring
MVC or Spring context libraries to every leaf. A test dependency belongs in the leaf that uses it.
## 9. External Dependency Purity Gate
The logging-boundary plan first introduces `verifyApplicationCoreDependencyPurity`. This refactoring
then replaces that task with the registry-wide `verifyExternalDependencyPurity`; the two tasks do
not remain as overlapping gates. The replacement preserves the application main/test classpath
rules and `:application-core:check` wiring, then resolves each registered leaf's production
`compileClasspath` and `runtimeClasspath` and applies the broader role rules:
- `domain-core` and `shared-contract`: no external production module at all.
- `application-core`: no Spring, SLF4J/logging backend, JPA/Hibernate, servlet, transport, database,
cloud, or adapter implementation dependency.
- inbound/outbound adapters: no logging implementation dependency; SLF4J API is allowed.
- all production leaves: no test framework on production configurations.
The task reports `module → configuration → forbidden coordinate → rule`. It checks resolved
coordinates so transitive framework leakage is visible. Existing project-edge verification remains
separate and registry-driven.
Because the registry and harness API were absent at design time, the exact Python/Groovy/Java
implementation of registry membership, purity/processor gates, and architecture classpath wiring is
written in a post-recovery implementation addendum after the stable task packet is resolved. The
addendum must contain complete code against the recovered API and pass review before any of those
control-plane files are changed. Entry is fail-closed on a concrete overlay, an actual resolver
invocation, matching overlay/packet content hashes, and the resolved rule hash; `--help` output or a
prose-only confirmation is not packet evidence.
## 10. Registry-Driven Architecture Analysis
`app-bootstrap` gets an `architectureAnalysis` dependency bucket populated from every registered
production leaf, independent of `app-default` runtime membership. The architecture test runtime
extends this bucket; the application production runtime does not.
`CleanArchitectureTest` therefore sees GraphQL, gRPC, WebSocket, fileserver, objectstorage, Mongo,
and every other registered leaf. `SampleRemovalSmokeContractTest` reads production module paths from
the same registry instead of its current hard-coded list.
The architecture configuration is non-consumable and non-resolvable itself; only the dedicated test
runtime is resolvable. This prevents it from being published or accidentally used by `bootJar`.
## 11. Configuration Processor Consistency
The rule is mechanical:
- a leaf with main-source `@ConfigurationProperties` declares
`annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'`;
- a leaf without it does not.
A verification task scans registered main source roots and compares the result with the declared
annotation-processor dependency. This removes the unused GraphQL processor and adds missing
processors to settings-owning leaves without relying on a copied module list.
Binding/validation tests remain required for every settings class; generated metadata is not a
substitute for behavior tests. The post-recovery control-plane addendum inventories that mapping and
contains complete focused test code for every uncovered class before processor declarations change.
## 12. Runtime Composition and Component Scanning
`app-bootstrap` keeps only registry members marked `app-default` on its production runtime.
`opt-in` modules remain independently buildable and architecture-analyzed. Adoption of an opt-in
module is an explicit registry and composition-root change with its own focused tests.
`CaSkeletonApplication` narrows component and configuration-properties scanning to:
- `dev.caskeleton.bootstrap`
- `dev.caskeleton.adapter`
It removes `dev.caskeleton.application`, `dev.caskeleton.domain`, and `dev.caskeleton.shared` from
both scans. Those core packages own no Spring component or configuration-properties class, and the
outbox use case remains manually composed. A context test pins this boundary.
This is the smallest safe scan change in this refactoring. Converting all adapter configuration to
explicit `@Import` or auto-configuration is a separate design change.
## 13. Leaf-Specific Cleanup
All 19 leaves receive a characterization record and focused command:
| Leaf | Target decision |
| --- | --- |
| `domain-core` | preserve zero-framework main; isolate pure tests |
| `application-core` | consume the approved outbox-reporting result; verify Boot/SLF4J remain absent |
| `shared-contract` | preserve stdlib-only production graph |
| `adapter:inbound:web` | remove only compile-proven unused core edge; retain transport dependencies |
| `adapter:inbound:graphql` | remove compile-proven core/tooling candidates |
| `adapter:inbound:grpc` | retain server/health/reflection runtime; remove no-stub candidates only after compile |
| `adapter:inbound:websocket` | retain domain-event and WebSocket dependencies; prune unused core edges |
| `adapter:outbound:support` | retain only compile-proven core edges and SLF4J API/autoconfigure |
| `adapter:outbound:cache-redis` | remove unused core edges and unused Groovy/Spock |
| `adapter:outbound:fileserver` | keep application/shared ports; narrow starter only after characterization |
| `adapter:outbound:httpclient` | keep shared/support and actual Groovy/Spock tests; prune unused core edges |
| `adapter:outbound:identifier` | keep application pseudonymizer port; remove unused domain/uuid-creator |
| `adapter:outbound:messaging` | retain the approved outbox reporter plus application/shared/support; remove unused domain and Groovy/Spock |
| `adapter:outbound:notification` | keep application/shared/support; remove unused domain and Groovy/Spock |
| `adapter:outbound:objectstorage` | keep application/shared/AWS SDK; narrow starter only after characterization |
| `adapter:outbound:persistence-jpa` | keep application/shared/JPA/vendor runtime; test domain/Flyway candidates |
| `adapter:outbound:persistence-mongo` | remove all adapter-local `Example*` types/tests; keep generic opt-in config/properties and binding/disabled-mode tests; remove application/shared edges |
| `app-bootstrap` | preserve composition role; separate architecture classpath; narrow scans and duplicate tests |
| `sample-portfolio` | preserve fixture-only isolation; remove generated jqwik state and own sample-only dependencies |
## 14. Generated jqwik State
`src/sample-portfolio/.jqwik-database` is deleted from version control.
`src/.gitignore` ignores `.jqwik-database` at any module working directory. Property tests remain
deterministic from committed seeds/configuration rather than a developer-machine serialization
cache.
## 15. Shared ThreadLocal Decision
`ThreadLocalDomainContextPropagator` and `DomainContextPropagatorFactory` stay in
`shared-contract` for this refactoring. They are Java-stdlib-only operational infrastructure, and
moving them changes concurrency composition rather than dependency hygiene.
This is a deliberate secondary decision, not an accidental omission. The purity gate pins their
zero-external-dependency status. Relocation to bootstrap or an adapter requires a separate design
with virtual-thread/context-propagation characterization and is not bundled into graph cleanup.
## 16. Persistence Mongo Decision
The production Mongo leaf removes:
- `ExampleRecord`
- `ExampleMongoDocument`
- `ExampleMongoMapper`
- `ExampleMongoRepository`
- `ExampleMongoRepositoryAdapter`
- their example mapping/repository tests
`MongoPersistenceConfig` and `MongoPersistenceProperties` remain as generic opt-in Spring Mongo
machinery. Tests cover properties binding, disabled-by-default behavior, and mock-backed
enabled-mode creation of one Boot 4 `MongoClient` and `MongoTemplate` without a network connection
or sample repository.
No duplicate Mongo domain is added to `sample-portfolio`; the WorkLog JPA sample remains the sole
reference business domain. The Mongo leaf then removes its unused application/shared project edges.
## 17. Locking and Version Ownership
After each leaf cleanup:
1. run its compile and focused test;
2. regenerate its lock state through the repository `resolveAndLockAll --write-locks` entrypoint;
3. run `verifyDependencyLocks`;
4. inspect that removed coordinates disappeared from production configurations.
`check` or the CI release gate invokes `verifyDependencyLocks`. Spring Boot BOM owns managed Spring,
Jackson, Micrometer, Testcontainers, and related versions. Existing root extension values own gRPC,
protobuf, and AWS BOM versions. Every remaining non-BOM direct version has one root-level owner.
The build remains Groovy DSL with the current root configuration during this work. A version catalog
or convention-plugin migration is considered only after the complete refactoring and full `check`
are green, so build-system migration cannot mask dependency-removal regressions.
## 18. Verification Strategy
Verification proceeds from narrow to broad:
1. CI/harness prerequisite commands.
2. Registry schema, cycle, membership, and 19-leaf parity tests.
3. Before/after dependency reports for each candidate leaf.
4. Leaf `compileJava`, focused test, and configuration-processor check.
5. Pure-core external dependency gate.
6. Registry-driven project dependency and architecture tests.
7. Sample-off and optional-runtime composition tests.
8. Dependency lock verification.
9. Full `test` and `check`.
No removal is accepted when a focused command is skipped without a named environmental reason and
recorded residual risk.
## 19. Acceptance Criteria
- All four prerequisite commands pass before hygiene edits begin.
- Registry validation reports exactly 19 unique, acyclic leaves and owns runtime membership.
- No copied production-module list remains in architecture or sample-isolation tests.
- The approved outbox failure-reporting plan is green before hygiene pruning starts.
- `application-core` production dependencies continue to contain no Spring or logging coordinate.
- Domain, application, and shared tests run without Spring MVC/context dependencies.
- Every production leaf is present on the architecture-analysis test runtime.
- No production configuration depends on `sample-portfolio`.
- Every adapter peer edge is explicitly allowed by the source module's registry entry; the current
graph's peer edges all target `adapter:outbound:support`.
- Configuration processor declarations exactly match main-source properties classes.
- Every removed project/external dependency has before/after compile and focused-test evidence.
- Mongo production source contains no `Example*` domain/document/repository/mapper type.
- `.jqwik-database` is untracked and ignored.
- `verifyExternalDependencyPurity`, `verifyCleanArchitectureDependencies`, architecture tests,
`verifyDependencyLocks`, `test`, and `check` pass.
- No convention-plugin or version-catalog migration is included.
- Agents do not stage, commit, amend, or push; commit policy remains human-only.