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 – Updates tauri.conf.json and simulation source files with the current project version
  • writeVersion – Generates version.properties in app/common/src/main/resources for 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 in settings.gradle with physical directory mappings.
  • The root build.gradle centralizes version management via ext properties, applies common plugins through subprojects, and conditionally includes the proprietary module based on environment variables.
  • Only the :stirling-pdf module produces a runnable artifact; it configures bootJar with zip64 support and conditionally embeds React frontend assets when -PbuildWithFrontend=true is passed.
  • The :common module provides transitive dependencies via api declarations and disables bootRun, while the :proprietary module is excluded from security-hardened builds when DOCKER_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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →