21 KiB
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:
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:
src/settings.gradlecannot configure without.harness/project/modules.yaml.- There are exactly 19 leaf
build.gradlefiles and 19 leafgradle.lockfilefiles. - No production configuration depends on
:sample-portfolio;app-bootstraphas one test-onlysampleFixtureedge. - The only adapter-to-adapter project edges are:
messaging,cache-redis,notification, andhttpclienttoadapter:outbound:support. domain-coreandshared-contractmain source contain no Spring, JPA, Jackson, or SLF4J imports.application-coremain source contains no Spring import. Its only framework imports are SLF4J inPublishPendingOutboxEventsUseCase.- The root build adds Spring Boot test and Spring MVC test starters to every leaf.
cache-redis,messaging, andnotificationhave no Groovy tests although their builds apply Groovy and add Spock.adapter:outbound:identifierdoes not useuuid-creator.src/sample-portfolio/.jqwik-databaseis a tracked Java-serialization runtime artifact.persistence-mongoowns adapter-localExample*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-starterusage 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
- Keep all module paths and allowed edges in
.harness/project/modules.yamlonly. - Make the actual project DAG the smallest graph required by source, tests, and runtime composition.
- Preserve the approved outbox failure-reporting refactor's removal of Spring and logging
frameworks from
application-corecompile/runtime classpaths. - Give
domain-core,application-core, andshared-contractframework-free test conventions. - Enforce external dependency purity for core modules from resolved compile/runtime graphs.
- Analyze every registered production leaf with the architecture suite regardless of runtime composition.
- Apply the Spring configuration processor exactly where main source declares
@ConfigurationProperties. - Verify strict locks in the release gate and assign one owner to every non-BOM version.
- State which optional adapters are in the default app runtime and which are opt-in.
- Remove generated jqwik state from source control.
- 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 defaultapp-bootstrapruntime 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-bootstraporsample-portfolioitself.
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-coreownsOutboxRelayFailureReportPort;- 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:messagingowns the structured SLF4J reporter implementation;app-bootstrapinjects 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-coreandshared-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
@ConfigurationPropertiesdeclaresannotationProcessor '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.bootstrapdev.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:
ExampleRecordExampleMongoDocumentExampleMongoMapperExampleMongoRepositoryExampleMongoRepositoryAdapter- 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:
- run its compile and focused test;
- regenerate its lock state through the repository
resolveAndLockAll --write-locksentrypoint; - run
verifyDependencyLocks; - 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:
- CI/harness prerequisite commands.
- Registry schema, cycle, membership, and 19-leaf parity tests.
- Before/after dependency reports for each candidate leaf.
- Leaf
compileJava, focused test, and configuration-processor check. - Pure-core external dependency gate.
- Registry-driven project dependency and architecture tests.
- Sample-off and optional-runtime composition tests.
- Dependency lock verification.
- Full
testandcheck.
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-coreproduction 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-databaseis untracked and ignored.verifyExternalDependencyPurity,verifyCleanArchitectureDependencies, architecture tests,verifyDependencyLocks,test, andcheckpass.- No convention-plugin or version-catalog migration is included.
- Agents do not stage, commit, amend, or push; commit policy remains human-only.