How the Multi-Module Gradle Build Works in Stirling-PDF
Stirling-PDF uses a three-module Gradle build—:stirling-pdf, :common, and :proprietary—where the root project coordinates shared versions, conditionally includes optional closed-source features, and aggregates tasks so that only the core module produces a runnable Spring Boot JAR.
The multi-module Gradle build in Stirling-PDF cleanly separates the main application, shared utilities, and optional proprietary extensions. This architecture allows developers to build a minimal backend-only artifact or a full-featured distribution with the React frontend embedded, while optionally excluding closed-source code for security-hardened deployments.
Project Structure and Module Overview
The repository is organized into three logical modules registered in the root settings.gradle. Each module maps to a physical directory under app/ and serves a distinct purpose in the build lifecycle.
Module Declaration in settings.gradle
The root settings.gradle registers the sub-projects and maps their logical names to physical directories:
rootProject.name = 'Stirling PDF'
include 'stirling-pdf', 'common', 'proprietary'
project(':stirling-pdf').projectDir = file('app/core')
project(':common' ).projectDir = file('app/common')
project(':proprietary' ).projectDir = file('app/proprietary')
This structure allows developers to reference modules by name (e.g., project(':common')) while keeping the source organized under app/core, app/common, and app/proprietary.
Logical Module Mapping
| Module | Directory | Purpose |
|---|---|---|
:stirling-pdf |
app/core |
Main Spring Boot application; produces the executable JAR and optionally embeds the React UI |
:common |
app/common |
Shared libraries (PDFBox wrappers, sanitizers, metadata extractors) used by all modules |
:proprietary |
app/proprietary |
Optional closed-source features; conditionally included based on security flags |
Root Build Configuration (build.gradle)
The root build.gradle acts as the central coordination point for the multi-module Gradle build. It defines shared versions, applies common plugins to all sub-projects, and implements conditional logic for including the proprietary module.
Shared Version Management
The root project uses ext properties to centralize version numbers, ensuring consistency across modules:
ext {
springBootVersion = "4.0.3"
pdfboxVersion = "3.0.6"
modernJavaVersion = 21
}
Sub-projects reference these properties in their dependency declarations, creating a single source of truth for library versions.
Conditional Module Inclusion
The build uses environment variables and project properties to conditionally include the proprietary module. This allows security-hardened builds to exclude closed-source code:
ext.isSecurityDisabled = { ->
System.getenv('DOCKER_ENABLE_SECURITY') == 'false' ||
System.getenv('DISABLE_ADDITIONAL_FEATURES') == 'true' ||
(project.hasProperty('DISABLE_ADDITIONAL_FEATURES') &&
System.getProperty('DISABLE_ADDITIONAL_FEATURES') == 'true')
}
if (rootProject.ext.isSecurityDisabled()) {
implementation project(':proprietary')
}
When DOCKER_ENABLE_SECURITY is set to false or DISABLE_ADDITIONAL_FEATURES is true, the proprietary module is excluded from the build.
Aggregated Build Tasks
The root project defines custom tasks to synchronize versions across the backend, desktop (Tauri), and simulation configurations:
syncAppVersion– Updatestauri.conf.jsonand simulation source files with the current project versionwriteVersion– Generatesversion.propertiesinapp/common/src/main/resourcesfor runtime access
These tasks are hooked into the processResources lifecycle:
tasks.register('syncAppVersion') { … }
tasks.register('writeVersion', WriteProperties) { … }
The root build task aggregates the core JAR, restart helper, and version synchronization:
tasks.named('build') {
dependsOn ':stirling-pdf:bootJar', 'buildRestartHelper', 'syncAppVersion'
}
Core Module: stirling-pdf (app/core)
The :stirling-pdf module is the only module configured to produce a runnable artifact. It contains the Spring Boot application, REST controllers, and optional embedded React frontend assets.
Spring Boot JAR Configuration
The core module explicitly enables bootJar and disables the standard jar task to ensure only a single executable artifact is produced:
bootJar {
enabled = true
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
zip64 = true
exclude 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA', 'META-INF/*.EC'
manifest {
attributes('Implementation-Title': 'Stirling-PDF',
'Implementation-Version': project.version)
}
}
jar { enabled = false }
The zip64 = true setting allows the JAR to exceed the 4GB limit, necessary for distributions containing multiple PDF engines and language packs.
Frontend Integration
The build supports conditional frontend compilation via the buildWithFrontend project property:
def buildWithFrontend = project.hasProperty('buildWithFrontend') && project.property('buildWithFrontend') == 'true'
if (buildWithFrontend) {
processResources.dependsOn copyFrontendAssets
} else {
processResources.dependsOn copyApiLandingPage
}
When buildWithFrontend=true, custom tasks (npmInstall, npmBuild, copyFrontendAssets) compile the React application and copy the resulting static files into src/main/resources/static. When disabled, a minimal API landing page is served instead.
Common and Proprietary Modules
Shared Libraries (app/common)
The :common module acts as a library dependency for the other modules. It disables the Spring Boot run task since it is not an executable application:
bootRun { enabled = false }
Dependencies are declared using the api configuration, making them transitively available to consumers:
api 'org.apache.pdfbox:pdfbox:$pdfboxVersion'
api 'org.springframework.boot:spring-boot-starter-webmvc'
This module houses PDFBox wrappers, HTML sanitizers, and metadata extraction utilities used by both the core application and optional proprietary extensions.
Optional Extensions (app/proprietary)
The :proprietary module contains closed-source or optional features. It follows the same structure as :common but is conditionally included in the multi-module Gradle build based on the isSecurityDisabled predicate. When excluded, the build produces a fully open-source artifact; when included, additional enterprise features are compiled into the final JAR.
Build Commands and Workflows
| Goal | Command | Description |
|---|---|---|
| Full build with UI | ./gradlew clean build -PbuildWithFrontend=true |
Compiles all modules, builds React frontend, and packages everything into the Spring Boot JAR |
| Backend-only build | ./gradlew clean build |
Builds core and common modules only; serves minimal API landing page |
| Run application | ./gradlew bootRun |
Starts the Spring Boot server on port 8080 via the core module |
| Generate OpenAPI | ./gradlew generateOpenApiDocs |
Creates SwaggerDoc.json using the SpringDoc plugin |
| Sync versions | ./gradlew syncAppVersion |
Updates Tauri and simulation configurations with current version |
| Build specific module | ./gradlew :stirling-pdf:bootJar |
Produces only the runnable JAR without full aggregation |
Summary
- The multi-module Gradle build in Stirling-PDF organizes code into three logical modules—
:stirling-pdf,:common, and:proprietary—registered insettings.gradlewith physical directory mappings. - The root
build.gradlecentralizes version management viaextproperties, applies common plugins throughsubprojects, and conditionally includes the proprietary module based on environment variables. - Only the
:stirling-pdfmodule produces a runnable artifact; it configuresbootJarwithzip64support and conditionally embeds React frontend assets when-PbuildWithFrontend=trueis passed. - The
:commonmodule provides transitive dependencies viaapideclarations and disablesbootRun, while the:proprietarymodule is excluded from security-hardened builds whenDOCKER_ENABLE_SECURITY=false.
Frequently Asked Questions
How do I build Stirling-PDF without the proprietary module?
Set the environment variable DOCKER_ENABLE_SECURITY to false or define the project property DISABLE_ADDITIONAL_FEATURES=true. The root build.gradle evaluates the isSecurityDisabled closure and excludes the :proprietary module from the dependency graph, producing a fully open-source artifact.
Why does only the core module produce a JAR file?
The :stirling-pdf module explicitly enables bootJar and disables the standard jar task, while :common and :proprietary are configured as library modules with bootRun disabled. This ensures that only one executable Spring Boot artifact is generated, avoiding confusion between runnable and library JARs.
How do I include the React frontend in the build?
Pass the -PbuildWithFrontend=true flag when running Gradle. This triggers the npmInstall and npmBuild tasks, compiles the React application, and copies the resulting static assets into src/main/resources/static via copyFrontendAssets. Without this flag, the build includes only a minimal API landing page.
What is the purpose of the common module?
The :common module houses shared utilities such as PDFBox wrappers, HTML sanitizers, and metadata extractors that are reused across the core application and optional proprietary extensions. It declares dependencies using the api configuration, making them transitively available to any module that depends on :common, while disabling Spring Boot-specific tasks since it is not an executable application.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →