Adding a New Module
Optional integration modules follow a consistent pattern established by fun-jackson and fun-assertj.
The design rationale — why each integration is a separate subproject with a compileOnly peer
dependency, how the BOM fits in, and the alternatives considered — is documented in
ADR-022 — Integration modules as optional peer dependencies.
This page is a checklist for maintainers adding a new module.
When to create a module
Create a new module when the integration requires an optional peer dependency that users may or may not
have on their classpath. The core lib module must never depend on it.
Examples of appropriate modules: fun-spring, fun-micronaut, fun-quarkus, fun-reactor.
Checklist
1. Create the Gradle subproject
Create the directory and build.gradle:
plugins { id 'dmx-fun.java-module'}
dependencies { api project(':lib') compileOnly libs.spring.context // optional peer dep testImplementation "org.springframework:spring-context:${findProperty('springVersion') ?: libs.versions.spring.get()}"}
// Wire JPMS test flags automatically via the convention plugin DSL.// moduleName is required; extraModules/extraReads/extraOpens are optional.jpmsTest { moduleName = 'dmx.fun.newmod'}
mavenPublishing { coordinates("codes.domix", "fun-newmod", "${project.version}")
pom { name = 'dmx-fun Newmod integration' description = 'Newmod module for dmx-fun: ...' }}Key rules:
- Always apply
dmx-fun.java-module— this pulls in the convention plugin, the Maven publishing configuration, the signing setup, and the JSpecify dependency. - Declare the peer dependency as
compileOnlyso users bring their own version. - Drive the test dependency version via a Gradle property (e.g.
-PspringVersion=6.1.0) to enable CI matrix testing, falling back to the version catalog entry.
2. Register in settings.gradle
include('fun-spring')3. Register in the root moduleMap
The root build.gradle uses a moduleMap as the single source of truth for all submodules.
Adding one entry here automatically wires the module into test-report aggregation, JaCoCo
coverage aggregation, and the aggregateJavadoc task — no other changes to the root build file
are needed.
// build.gradle (root)def moduleMap = [ ':lib' : 'dmx.fun', ':assertj' : 'dmx.fun.assertj', ':jackson' : 'dmx.fun.jackson', ':spring' : 'dmx.fun.spring', ':fun-newmod' : 'dmx.fun.newmod', // ← add this line]4. Configure JPMS tests
Every module ships a module-info.java — this is a deliberate architectural decision documented in
ADR-002 — JPMS from day one.
The dmx-fun.java-module convention plugin exposes a jpmsTest DSL block that wires
--module-path, --patch-module, and --add-modules into compileTestJava and test
automatically. Add it to the module’s build.gradle:
// Minimal — only moduleName is requiredjpmsTest { moduleName = 'dmx.fun.newmod'}
// Extended — for modules with extra JPMS requirementsjpmsTest { moduleName = 'dmx.fun.newmod' extraModules = ['java.sql'] // added to --add-modules extraReads = ['java.sql'] // generates --add-reads moduleName=X extraOpens = ['ALL-UNNAMED', // plain string → opens module's root package to target 'spring.core', // (--add-opens moduleName/moduleName=target) 'dmx.fun.x=spring.tx'] // 'pkg=target' form → opens pkg to target}| Property | Type | Required | Description |
|---|---|---|---|
moduleName | String | yes | JPMS module name (dmx.fun.*) |
extraModules | List<String> | no | Extra --add-modules entries added at both compile and runtime |
extraReads | List<String> | no | Extra --add-reads moduleName=X entries |
extraOpens | List<String> | no | Plain string: --add-opens moduleName/moduleName=target (root package only). 'pkg=target' form: --add-opens moduleName/pkg=target (named subpackage). Malformed 'pkg=target' entries (empty package or target) throw a GradleException at configuration time. |
testRuntimeOnlydependencies: thetesttask builds the module-path fromruntimeClasspath, sotestRuntimeOnlyentries (e.g. a JDBC driver declared astestRuntimeOnly libs.postgresql) are correctly visible to the JVM at test time. No extrajpmsTestconfiguration is needed for runtime-only dependencies.
5. Expand CI path triggers
Four workflow files reference module paths — update all of them:
| Workflow | Where to add |
|---|---|
.github/workflows/gradle.yml | paths: block (push) and dorny/paths-filter filters |
.github/workflows/publish.yml | paths: block |
.github/workflows/publish-snapshot.yml | paths: block |
6. Add a compatibility matrix workflow
If the module has an optional peer dependency, create .github/workflows/{module}-compatibility.yaml
following the pattern of jackson-compatibility.yaml:
- Trigger: PR touching
{module}/**or the workflow file - Matrix: one entry per supported version of the peer dependency
- Run:
./gradlew :{module}:test -P{module}Version={version}
7. Write a README.md in the module root
Cover: what the module does, Maven/Gradle coordinates, usage snippet, version compatibility table.
See jackson/README.md or assertj/README.md for the expected format.
8. Add a guide page
Create site/src/data/guide/{module-name}.mdx following the established guide style:
ordershould be the next available integer after the last module guide- Externalize all code snippets into
site/src/data/code/guide/{module-name}/ - Include a version compatibility table matching the CI matrix
9. Register in the guide index
Add a card to the modules array in site/src/pages/guide/index.astro:
{ name: 'Spring integration', href: `${base}guide/fun-spring`, summary: 'Spring-aware adapters for dmx-fun types.', whenToUse: 'You are using Spring and want native dmx-fun support.', avoidWhen: 'You are not using Spring — the module is optional.', tag: 'Spring',},Naming conventions
| Artifact | Pattern | Example |
|---|---|---|
| Directory | {integration-name} | fun-spring |
artifactId | fun-{integration-name} | fun-spring |
POM name | dmx-fun {Integration} integration | dmx-fun Spring integration |
| Java module name | dmx.fun.{integration} | dmx.fun.spring |
| Base package | dmx.fun.{integration} | dmx.fun.spring |
| Guide slug | {integration-name} | /guide/fun-spring |