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 compileOnly so 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 required
jpmsTest {
moduleName = 'dmx.fun.newmod'
}
// Extended — for modules with extra JPMS requirements
jpmsTest {
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
}
PropertyTypeRequiredDescription
moduleNameStringyesJPMS module name (dmx.fun.*)
extraModulesList<String>noExtra --add-modules entries added at both compile and runtime
extraReadsList<String>noExtra --add-reads moduleName=X entries
extraOpensList<String>noPlain 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.

testRuntimeOnly dependencies: the test task builds the module-path from runtimeClasspath, so testRuntimeOnly entries (e.g. a JDBC driver declared as testRuntimeOnly libs.postgresql) are correctly visible to the JVM at test time. No extra jpmsTest configuration is needed for runtime-only dependencies.

5. Expand CI path triggers

Four workflow files reference module paths — update all of them:

WorkflowWhere to add
.github/workflows/gradle.ymlpaths: block (push) and dorny/paths-filter filters
.github/workflows/publish.ymlpaths: block
.github/workflows/publish-snapshot.ymlpaths: 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:

  • order should 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

ArtifactPatternExample
Directory{integration-name}fun-spring
artifactIdfun-{integration-name}fun-spring
POM namedmx-fun {Integration} integrationdmx-fun Spring integration
Java module namedmx.fun.{integration}dmx.fun.spring
Base packagedmx.fun.{integration}dmx.fun.spring
Guide slug{integration-name}/guide/fun-spring