Packaging Java Apps: Fat JAR with Shade Plugin vs Spring Boot JAR vs jpackage

Distributing a Java application means choosing how to package it. Should you produce a thin JAR and let users manage the classpath? An über JAR (fat JAR) with all dependencies bundled in? A native installer via jpackage? Each approach has a place, and understanding all three lets you pick the right one for every deployment scenario — from a developer tool shipped on Maven Central to a desktop app distributed to non-technical users.

Option 1 — Executable Fat JAR with Maven Shade Plugin

The Maven Shade Plugin merges all dependency JARs into a single über JAR. The result is one file that can be run anywhere Java is installed:

java -jar my-app-1.0-shaded.jar
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-shade-plugin</artifactId>
      <version>3.5.2</version>
      <executions>
        <execution>
          <phase>package</phase>
          <goals><goal>shade</goal></goals>
          <configuration>
            <createDependencyReducedPom>false</createDependencyReducedPom>
            <transformers>
              <!-- Sets the Main-Class in MANIFEST.MF -->
              <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                <mainClass>com.ankurm.App</mainClass>
              </transformer>
              <!-- Merges META-INF/services files (needed for SPI) -->
              <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
            </transformers>
            <filters>
              <filter>
                <artifact>*:*</artifact>
                <excludes>
                  <!-- Remove signature files to avoid JAR verification errors -->
                  <exclude>META-INF/*.SF</exclude>
                  <exclude>META-INF/*.DSA</exclude>
                  <exclude>META-INF/*.RSA</exclude>
                </excludes>
              </filter>
            </filters>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

Build and run:

mvn clean package
java -jar target/my-app-1.0-shaded.jar

Option 2 — Fat JAR with Spring Boot Maven Plugin

Spring Boot’s plugin produces a nested über JAR that avoids classpath conflicts because each dependency’s JAR remains intact inside the outer JAR:

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      <configuration>
        <!-- Optional: exclude dev tools from the fat JAR -->
        <excludes>
          <exclude>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
          </exclude>
        </excludes>
      </configuration>
    </plugin>
  </plugins>
</build>
mvn clean package
java -jar target/my-app-1.0-SNAPSHOT.jar

Option 3 — Native Platform Installer with jpackage

jpackage (JDK 14+, stable in JDK 16+) bundles your application with a trimmed JRE and produces a platform-native installer: .msi/.exe on Windows, .dmg/.pkg on macOS, .deb/.rpm on Linux. Users do not need Java installed.

<plugin>
  <groupId>org.panteleyev</groupId>
  <artifactId>jpackage-maven-plugin</artifactId>
  <version>1.6.5</version>
  <configuration>
    <name>MyApp</name>
    <appVersion>1.0.0</appVersion>
    <vendor>AnkurM Studio</vendor>
    <destination>target/installer</destination>
    <module>com.ankurm/com.ankurm.App</module>
    <runtimeImage>${project.build.directory}/runtime</runtimeImage>
    <javaOptions>
      <option>-Dfile.encoding=UTF-8</option>
    </javaOptions>
    <!-- Windows-specific -->
    <winDirChooser>true</winDirChooser>
    <winMenu>true</winMenu>
    <winMenuGroup>AnkurM Studio</winMenuGroup>
    <winShortcut>true</winShortcut>
  </configuration>
</plugin>

Build a trimmed JRE first with jlink, then package:

# Step 1: Create a minimal runtime image
jlink --module-path $JAVA_HOME/jmods 
      --add-modules java.base,java.desktop,java.sql 
      --output target/runtime

# Step 2: Build the installer
mvn jpackage:jpackage
# produces target/installer/MyApp-1.0.0.msi (Windows)
# or target/installer/MyApp-1.0.0.dmg (macOS)

Side-by-Side Comparison

FeatureShade Fat JARSpring Boot JARjpackage Installer
Java required on target?YesYesNo (bundled JRE)
Output sizeMediumMediumLarge (JRE included)
Classpath conflicts?PossibleNo (nested JARs)N/A
Best forCLI tools, librariesSpring Boot appsDesktop apps, non-devs
Cross-platform?Yes (single JAR)Yes (single JAR)No (build per OS)
Requires JDK versionAnyAnyJDK 14+

Common Pitfalls

  • Shade: duplicate resources — two JARs with the same META-INF/services entry overwrite each other unless you include ServicesResourceTransformer.
  • Shade: signed JARs — always exclude *.SF, *.DSA, *.RSA signature files or the JVM will refuse to run the merged JAR.
  • jpackage: module path — if your app is not fully modularised, use --type app-image with a class-path mode instead of module mode.
  • jpackage: platform lock-in — you must run jpackage on the same OS you are targeting; use CI runners for each platform.

See Also

Conclusion

Choose the Maven Shade Plugin for CLI tools and library distributions where you want a single portable JAR without framework overhead. Use the Spring Boot Maven Plugin for any Spring Boot application — its nested-JAR format avoids classpath conflicts and pairs perfectly with Docker. Reach for jpackage when deploying a desktop or GUI application to end-users who have no Java installation and expect a familiar platform installer. All three approaches can coexist in different Maven profiles so you can build the right artifact for each deployment target from the same codebase.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.