This demo shows how to keep one OpenAPI contract and still generate Spring Boot API code into separate Maven modules by filtering which APIs are generated per module.
Additionally, each module is runnable as its own Spring Boot application (no separate “app module”).
- Keep a single OpenAPI spec:
doc/contract/openapi.yaml - Group endpoints using OpenAPI tags
tags: [ModuleA]→ generates API groupModuleA(e.g.,ModuleAApi)tags: [ModuleB]→ generates API groupModuleB(e.g.,ModuleBApi)
- Run the OpenAPI generator twice (once per module) against the same spec, filtering by API group:
module-a-apigenerates onlyModuleAmodule-b-apigenerates onlyModuleB
Generator:
org.openapitools:openapi-generator-maven-plugin
Filter mechanism:globalProperties.apis
demo-openapi-multi-module/
├── pom.xml
├── doc/
│ └── contract/
│ └── openapi.yaml
├── module-a-api/
│ ├── pom.xml
│ └── src/main/java/
│ └── com/example/demo/modulea/
│ ├── ModuleAApiApplication.java
│ └── impl/ModuleAApiControllerImpl.java
└── module-b-api/
├── pom.xml
└── src/main/java/
└── com/example/demo/moduleb/
├── ModuleBApiApplication.java
└── impl/ModuleBApiControllerImpl.java
Contract file:
doc/contract/openapi.yaml
The spec should tag operations like:
/a/...endpoints taggedModuleA/b/...endpoints taggedModuleB
With useTags=true, the generator groups operations by tag and typically creates interfaces like:
ModuleAApiModuleBApi
Both modules point to the same inputSpec but filter on different API groups:
inputSpec=../doc/contract/openapi.yamlglobalProperties.apis=ModuleA
inputSpec=../doc/contract/openapi.yamlglobalProperties.apis=ModuleB
This ensures each module only generates its own API interfaces/controllers.
Instead of a separate runnable app module, each API module contains its own Spring Boot entry point.
module-a-api/src/main/java/com/example/demo/modulea/ModuleAApiApplication.java
Configured to scan only Module A packages so only Module A controllers are loaded:
@SpringBootApplication(scanBasePackages = "com.example.demo.modulea")module-b-api/src/main/java/com/example/demo/moduleb/ModuleBApiApplication.java
Scans only Module B packages:
@SpringBootApplication(scanBasePackages = "com.example.demo.moduleb")Each module implements its generated interface in a Spring @RestController, e.g.:
- Module A:
ModuleAApiControllerImpl implements ModuleAApi - Module B:
ModuleBApiControllerImpl implements ModuleBApi
Note: exact method names/signatures depend on generator settings. Always match the generated interface method signature.
From repository root:
mvn clean installThis will:
- generate sources for
module-a-apiandmodule-b-api - compile both modules
From repository root:
mvn -pl module-a-api spring-boot:runTest:
curl http://localhost:8080/a/ping/b/ping should not exist (404), because module B is not running.
From repository root:
mvn -pl module-b-api spring-boot:runTest:
curl http://localhost:8080/b/ping/a/ping should not exist (404), because module A is not running.
-
API naming depends on tags
- The
<apis>ModuleA</apis>and<apis>ModuleB</apis>values must match the generator’s API group names (usually derived from tags). - If your generator produces different names (e.g.,
Modulea), update the filter accordingly.
- The
-
Models / shared schemas
- If both modules generate the same model classes, you can get duplicate classes across jars.
- Common approaches:
- Generate models in a separate
shared-modelmodule (recommended for large specs), OR - Disable model generation in leaf modules and depend on a shared jar, OR
- Accept duplicates if you never run them together (still not ideal).
- Generate models in a separate
-
Ports
- If you run module A and module B at the same time locally, configure different ports (e.g.,
--server.port=8081).
- If you run module A and module B at the same time locally, configure different ports (e.g.,
Example:
mvn -pl module-b-api spring-boot:run -Dspring-boot.run.arguments=--server.port=8081