Multi-module setup with outlet overrides
This guide shows how to use JoinedWorkz in a multi-module Maven project and route generated artifacts (Java sources, OpenAPI, diagrams, …) into different modules using outlet overrides. For generated Java classes and other output, CMN layers can select layer-specific routes.
The goal is:
- keep models in a dedicated module
- keep generated code close to the modules that use it (domain, API, documentation)
- avoid copying or manually moving generated files
It builds on:
- Maven plugin reference
- Facilities & platforms
- Integrate into an existing Java project
- Generated output, ownership and regeneration
Every routed directory must have an explicit owner and cleanup rule. Never route replaceable output into a directory that also contains manual, first-cut or versioned migration files.
This guide requires Java 21 and Maven 3.9+. The four-module
my-applayout is an architectural template. Adapt its module names and directories to your project.
1. Example project structure
We’ll use a typical layered project as running example:
my-app/
pom.xml (parent)
model/ (CMN models + JoinedWorkz plugin)
backend/ (domain + business logic)
webapp/ (Spring Boot application, controllers, HTTP APIs)
docs/ (optional: documentation / diagrams)High-level responsibilities:
- model
- contains CMN models (
.cmn) - runs the JoinedWorkz Maven plugin
- contains CMN models (
- backend
- contains domain code (entities, services, persistence)
- receives generated domain-related Java code
- webapp
- contains the Spring Boot application and HTTP boundary
- receives generated controller/API sources and OpenAPI
- docs (optional)
- receives generated diagrams or HTML OpenAPI viewers
Routing is configured in joinedworkz.properties in the root of the model module. A global outlet override is the fallback for files written to that outlet. A matching layer-specific override takes precedence.
2. Parent POM and module order
In my-app/pom.xml you declare the modules in a sensible order:
<modules>
<module>model</module>
<module>backend</module>
<module>webapp</module>
<module>docs</module>
</modules>Important:
- The model module must appear before the modules that consume generated artifacts (
backend,webapp,docs), so that generation runs first in a sequential reactor build.
Maven will then:
- build
model(runs the JoinedWorkz plugin) - place generated outputs into the configured directories (possibly in sibling modules)
- compile
backend,webapp,docsusing the generated code/resources
Do not rely on this list order as synchronization with mvn -T. Maven may compile otherwise independent consumer modules while the model module is still generating files. Use a sequential reactor build for filesystem-based cross-module output, or establish real Maven dependencies that order every consumer after the model module.
3. Model module setup
In model/pom.xml you configure:
- the JoinedWorkz facilities (e.g.
spring-boot) - the
cmn-maven-plugin - the
model/folder as a resource root
Example:
<project>
...
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<joinedworkz.version>1.3.81</joinedworkz.version>
</properties>
<dependencies>
<!-- Spring Boot facility (brings in Java + Base) -->
<dependency>
<groupId>org.joinedworkz.facilities</groupId>
<artifactId>spring-boot</artifactId>
<version>${joinedworkz.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>model</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.joinedworkz.cmn</groupId>
<artifactId>cmn-maven-plugin</artifactId>
<version>${joinedworkz.version}</version>
<executions>
<execution>
<?m2e ignore?><!-- ignore this execution in Eclipse -->
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>The model module itself typically does not contain Java production code. Its job is to:
- hold the CMN models
- run the generators
- route outputs into other modules
4. Layers and CMN package headers
Layers are declared in the header of CMN files. A declared CMN layer normally becomes the effective layer for generated output. A generator can assign a different effective layer to a particular file.
A common pattern in a multi-module Spring Boot project:
core– internal domain typesapi– external API DTOs and resources- a model without a layer – components, applications, technical backend configuration
Example CMN headers:
core package com.example.customer
import org.joinedworkz.facilities.common.base
import org.joinedworkz.facilities.common.base.api
import org.joinedworkz.facilities.profiles.springboot
platform SpringBootapi package com.example.customer.api
import com.example.customer
import org.joinedworkz.facilities.common.base.api
import org.joinedworkz.facilities.profiles.springboot
platform SpringBootpackage com.example.customer.backend
import com.example.customer.api
import org.joinedworkz.facilities.profiles.springboot
platform SpringBootTypical usage:
- core: entities, value objects, domain model
- api: DTOs (
CustomerView,OrderSummary, …) and resources - backend model without a layer: components (
component CustomerBackend), applications, etc.
This matches the structure of the Spring Boot example. Its component model has no layer, so generated outputs derived from that component do not receive effective layer backend merely because they represent backend code. Individual generators can still assign another effective layer; SpringBoot controller and API-interface output uses api.
5. Outlets: what you can override
Outlets define where generated artifacts are written, e.g.:
generatedJavaSource– Java sources (from Java/SpringBoot)generatedOpenApi– OpenAPI YAMLgeneratedOpenApiHtml– HTML OpenAPI viewergeneratedDiagram– diagrams
The Base and SpringBoot profiles define default directories, for example:
./src/generated/resources/openapi./diagram/api./diagram
In a multi-module setup these defaults are often not what you want. Use joinedworkz.properties to define a global directory for an outlet:
Pattern:
outlet.<outletName>.directory=...The configuration loader also accepts layer-specific mappings:
outlet.<outletName>.<layer>.directory=...Such a mapping is selected only when the generated file has the matching effective layer. The declared CMN layer is the default; a generator can assign a different effective layer. Output without an effective layer and output without an exact matching mapping use the global outlet route.
6. joinedworkz.properties – routing examples
Create joinedworkz.properties in the root of the model Maven module. In the example project tree this path is model/joinedworkz.properties; model/ is the module name here, not its CMN source directory.
6.1 Routing SpringBoot Java sources
Assume:
coretypes (domain) should go tobackend/src/generated/java- controller/API sources should go to
webapp/src/generated/java
# fallback for Java generators writing to generatedJavaSource
outlet.generatedJavaSource.directory=src/generated/java
# domain-related Java code (core layer) to backend module
outlet.generatedJavaSource.core.directory=../backend/src/generated/java
# SpringBoot controller/API output (effective layer api) to web module
outlet.generatedJavaSource.api.directory=../webapp/src/generated/javaWith the SpringBoot generators:
generatedJavaSource.corereceives the entity, repository, data access service, mapper and DTO classes derived from thecoremodel;generatedJavaSource.apireceives the generated controller and API interface. SpringBoot assigns effective layerapito this output even when the providing component model itself has no declared layer.
6.2 Routing OpenAPI and HTML viewers globally
OpenAPI and the HTML viewer usually live in the web boundary module (webapp):
# All OpenAPI YAML
outlet.generatedOpenApi.directory=../webapp/src/generated/resources/openapi
# All HTML OpenAPI viewers
outlet.generatedOpenApiHtml.directory=../webapp/diagram/apiThis example defines no layer-specific OpenAPI mappings. The global overrides therefore collect OpenAPI output from every effective layer. A matching layer-specific mapping would take precedence.
6.3 Routing diagrams to docs
If you have a docs module that collects diagrams:
# diagrams (e.g. component / application diagrams)
outlet.generatedDiagram.directory=../docs/diagramThis example likewise defines no layer-specific diagram mapping, so the global override collects diagrams from every effective layer. A matching layer-specific mapping would take precedence.
7. Configuring target modules
The modules that receive generated artifacts must register the directories in their POMs.
7.1 Backend module: generated Java sources
backend/pom.xml:
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<id>add-generated-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${basedir}/src/generated/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>You now can use generated domain types or DTOs from the backend module as if they were hand-written sources.
7.2 Webapp module: generated Java + resources
webapp/pom.xml should handle:
- generated controller/API Java sources
- generated resources (OpenAPI YAML, HTML)
<build>
<resources>
<!-- your existing resources -->
<resource>
<directory>src/main/resources</directory>
</resource>
<!-- generated resources (OpenAPI, etc.) -->
<resource>
<directory>src/generated/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<id>add-generated-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${basedir}/src/generated/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>Now:
- the generated controller/API sources become part of the webapp’s codebase
- the generated OpenAPI YAML is packaged as a resource, and can be served or used by other tools
7.3 Docs module: diagrams
In docs/pom.xml, register the diagram directory:
<build>
<resources>
<resource>
<directory>diagram</directory>
</resource>
</resources>
</build>You can then publish or serve the generated diagrams together with other documentation.
8. Build and verify
From the project root:
mvn clean packageFor a project adapted to this template, check the configured destinations:
model/- should contain only CMN models and
joinedworkz.properties - may have temporary build output (e.g.
target), but no production code
- should contain only CMN models and
backend/src/generated/java- should receive entity, repository, data access, mapper and DTO output routed from the
coremodel
- should receive entity, repository, data access, mapper and DTO output routed from the
webapp/src/generated/java- should receive controller/API output routed through effective layer
apiofgeneratedJavaSource
- should receive controller/API output routed through effective layer
webapp/src/generated/resources/openapi- should receive OpenAPI YAML through the configured global outlet
webapp/diagram/api- should receive HTML OpenAPI viewers through the configured global outlet
docs/diagram- should receive diagrams through the configured global outlet
If something is missing:
- check the outlet names used in
joinedworkz.properties - for layer-specific mappings, check which effective layer the generated file uses and whether it exactly matches the mapping name
- for SpringBoot controller/API code, check the
outlet.generatedJavaSource.api.directoryroute - for OpenAPI, OpenAPI HTML and diagrams that should be collected regardless of layer, check the global outlet override
- check that the target modules include the directories as sources/resources
9. Variants and tips
A few common variations:
Only OpenAPI, no Java If you only want centralised OpenAPI, you can:
- use global overrides for
generatedOpenApiandgeneratedOpenApiHtml - skip
generatedJavaSourceand source registration in target modules
- use global overrides for
Multiple bounded contexts Use separate packages per bounded context. Where generated Java classes need different targets, assign layers and configure matching
generatedJavaSourcelayer mappings. Configure other SpringBoot outlets separately. A layer-specific mapping applies only when the generated file has that effective layer.Platform combinations Follow the documented outlet names and any generator-assigned effective layers of every facility used. The CMN layer remains the default when a generator does not assign a different effective layer.
Gradual migration Start with a minimal multi-module setup (model + one target module), then introduce more modules, global outlet overrides and Java layer-specific mappings as the system evolves.
10. Summary
In a multi-module setup, outlet overrides control where generated artifacts end up:
- Keep models in a dedicated module that runs the JoinedWorkz plugin.
- Use layers such as
coreorapiin CMN headers to express architectural intent and select a matching layer-specific mapping. A generator can assign a different effective layer to individual output; a model may also deliberately declare no layer. - Configure
joinedworkz.propertiesto route:- generated Java classes by their effective layer,
- OpenAPI and OpenAPI HTML globally to a web/API module,
- diagrams globally to a docs module.
- Register generated directories as sources/resources in the target modules’ POMs.
- Let the multi-module build take care of calling the generators first and then compiling all modules against the generated code.
This keeps your project structure clean and lets you scale modeling and generation across multiple modules without losing track of where artifacts come from or where they belong.
