Skip to content

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:

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-app layout 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:

text
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
  • 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:

xml
<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:

  1. build model (runs the JoinedWorkz plugin)
  2. place generated outputs into the configured directories (possibly in sibling modules)
  3. compile backend, webapp, docs using 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:

xml
<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 types
  • api – external API DTOs and resources
  • a model without a layer – components, applications, technical backend configuration

Example CMN headers:

cmn
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 SpringBoot
cmn
api package com.example.customer.api

import com.example.customer
import org.joinedworkz.facilities.common.base.api
import org.joinedworkz.facilities.profiles.springboot

platform SpringBoot
cmn
package com.example.customer.backend

import com.example.customer.api
import org.joinedworkz.facilities.profiles.springboot

platform SpringBoot

Typical 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 YAML
  • generatedOpenApiHtml – HTML OpenAPI viewer
  • generatedDiagram – 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:

properties
outlet.<outletName>.directory=...

The configuration loader also accepts layer-specific mappings:

properties
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:

  • core types (domain) should go to backend/src/generated/java
  • controller/API sources should go to webapp/src/generated/java
properties
# 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/java

With the SpringBoot generators:

  • generatedJavaSource.core receives the entity, repository, data access service, mapper and DTO classes derived from the core model;
  • generatedJavaSource.api receives the generated controller and API interface. SpringBoot assigns effective layer api to 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):

properties
# All OpenAPI YAML
outlet.generatedOpenApi.directory=../webapp/src/generated/resources/openapi

# All HTML OpenAPI viewers
outlet.generatedOpenApiHtml.directory=../webapp/diagram/api

This 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:

properties
# diagrams (e.g. component / application diagrams)
outlet.generatedDiagram.directory=../docs/diagram

This 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:

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)
xml
<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:

xml
<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:

bash
mvn clean package

For 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
  • backend/src/generated/java

    • should receive entity, repository, data access, mapper and DTO output routed from the core model
  • webapp/src/generated/java

    • should receive controller/API output routed through effective layer api of generatedJavaSource
  • 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.directory route
  • 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 generatedOpenApi and generatedOpenApiHtml
    • skip generatedJavaSource and source registration in target modules
  • Multiple bounded contexts Use separate packages per bounded context. Where generated Java classes need different targets, assign layers and configure matching generatedJavaSource layer 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:

  1. Keep models in a dedicated module that runs the JoinedWorkz plugin.
  2. Use layers such as core or api in 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.
  3. Configure joinedworkz.properties to route:
    • generated Java classes by their effective layer,
    • OpenAPI and OpenAPI HTML globally to a web/API module,
    • diagrams globally to a docs module.
  4. Register generated directories as sources/resources in the target modules’ POMs.
  5. 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.