Integration modules as optional peer dependencies
Context
Many projects need dmx-fun types to interoperate with frameworks (Spring, Quarkus,
Jackson, Resilience4j, Micrometer, Jakarta). These integrations cannot live in the
core lib module because they would force transitive dependencies on all users,
even those who do not use the relevant framework. At the same time, users who adopt
several integrations must not have to track version numbers for each artifact
separately.
Decision
Package each integration as a separate Gradle subproject organized by category,
with the peer dependency declared compileOnly. Users declare the peer dependency
themselves; the integration module adapts dmx-fun types to the framework’s API.
A BOM (fun-bom) centralizes version alignment for users who adopt multiple modules.
Directory layout — subprojects are grouped by concern, not by framework name:
core/ lib/ → codes.domix:fun (always required) assertj/ → codes.domix:fun-assertjserialization/ jackson/ → codes.domix:fun-jackson jakarta-jaxb/ → codes.domix:fun-jakarta-jaxb jakarta-validation/→ codes.domix:fun-jakarta-validationobservability/ micrometer/ → codes.domix:fun-micrometer observation/ → codes.domix:fun-observation tracing/ → codes.domix:fun-tracingresilience/ resilience4j/ → codes.domix:fun-resilience4jprotocols/ http/ → codes.domix:fun-httpframeworks/ spring/ → codes.domix:fun-spring spring-boot/ → codes.domix:fun-spring-boot quarkus/runtime/ → codes.domix:fun-quarkusbom/ → codes.domix:fun-bomModule conventions — every integration subproject follows a shared pattern
enforced by the dmx-fun.java-module convention plugin:
- The peer dependency is declared
compileOnlyso it is never pulled in transitively. Users bring their own version. api project(':core:lib')exposes the core library transitively, so users only need one dependency declaration for both the integration and the core types.- Test dependencies use the concrete peer version resolved by a Gradle property
(e.g.,
-PjacksonVersion=X.Y.Z), falling back to the version catalog entry. This enables CI compatibility matrix jobs without modifying build files. - Every module ships a
module-info.java(JPMS), consistent with ADR-002 — JPMS from day one. Thedmx-fun.java-moduleplugin wires--module-path,--patch-module, and--add-modulesintocompileTestJavaandtestautomatically via thejpmsTestDSL block. - Maven coordinates follow
codes.domix:fun-{integration}with JPMS module namesdmx.fun.{integration}and base packagedmx.fun.{integration}. - The
dmx-fun.java-moduleplugin also wires Maven publishing, artifact signing, and the JSpecify null-safety dependency — no per-module boilerplate.
BOM — codes.domix:fun-bom is a standard Maven Bill of Materials that
constrains all dmx-fun artifact versions in one import. Users who adopt multiple
modules import the BOM once and omit the version from every individual declaration:
// Gradleimplementation(platform("codes.domix:fun-bom:${version}"))<!-- Maven --><dependencyManagement> <dependencies> <dependency> <groupId>codes.domix</groupId> <artifactId>fun-bom</artifactId> <version>${version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement>CI compatibility matrices — each integration module with a peer dependency
provides a dedicated workflow (e.g., jackson-compatibility.yaml) that runs the
module’s test suite against every supported version of the peer library using the
-P{framework}Version=X.Y.Z override property. The core lib and assertj modules
do not need a compatibility matrix because they have no peer dependency.
Consequences
Positive:
- Core
libhas zero framework dependencies — no transitive noise for users who do not need integrations. - Each integration module is versioned and published independently to Maven Central
under the same group ID (
codes.domix). - The BOM allows users to import all modules at a consistent version without repeating version numbers.
- CI matrix workflows per module verify compatibility across the supported version range of each peer dependency without modifying build files.
- The
dmx-fun.java-moduleconvention plugin eliminates per-module boilerplate: publishing, signing, JPMS wiring, and JSpecify are configured once. api project(':core:lib')means a single user dependency declaration is enough for both the integration and the core types.
Negative / tradeoffs:
- 13 integration subprojects (as of this writing) to maintain; each new module
needs its own directory,
build.gradle,module-info.java, guide page, and CI compatibility workflow. - Users must discover and add the correct integration artifact for their framework;
the core
libdoes not advertise optional integrations. compileOnlypeer dependencies are absent from the published POM — tools that inspect the POM (e.g., Dependabot) will not surface peer dependency updates.
Alternatives considered
- Optional Maven dependencies in a single artifact: pollutes the compile
classpath even for unused integrations; the
optionalflag has poor and inconsistent support in Gradle and IDEs. - One multi-classifier artifact: non-standard; breaks standard Maven/Gradle dependency resolution and confuses IDEs.
- Fat jar with shading: completely inappropriate for a library — forces version conflicts on consumers by relocating shared classes.
- Flat module list (no category directories): all subprojects at the root level;
rejected because the 13+ modules would be difficult to navigate and the category
grouping (
frameworks/,serialization/,observability/) makes the project structure self-documenting.