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
| Feature | Shade Fat JAR | Spring Boot JAR | jpackage Installer |
|---|---|---|---|
| Java required on target? | Yes | Yes | No (bundled JRE) |
| Output size | Medium | Medium | Large (JRE included) |
| Classpath conflicts? | Possible | No (nested JARs) | N/A |
| Best for | CLI tools, libraries | Spring Boot apps | Desktop apps, non-devs |
| Cross-platform? | Yes (single JAR) | Yes (single JAR) | No (build per OS) |
| Requires JDK version | Any | Any | JDK 14+ |
Common Pitfalls
- Shade: duplicate resources — two JARs with the same
META-INF/servicesentry overwrite each other unless you includeServicesResourceTransformer. - Shade: signed JARs — always exclude
*.SF,*.DSA,*.RSAsignature files or the JVM will refuse to run the merged JAR. - jpackage: module path — if your app is not fully modularised, use
--type app-imagewith a class-path mode instead of module mode. - jpackage: platform lock-in — you must run
jpackageon the same OS you are targeting; use CI runners for each platform.
See Also
- Building Native Installers for Java Applications with JPackage
- Mastering Maven: How to Create Your Own Custom Archetypes
- Building a REST API with Spring Boot
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.