Choosing Maven Build Artifacts: Shade vs Assembly and Centralized Version Control
When building multi‑module Maven projects you must decide how to package dependencies and keep versions consistent. This guide compares the Shade and Assembly plugins, explains Maven’s dependency mediation, and shows how to use <dependencyManagement> to lock versions across modules.
02 Feb 2026, 18:31 UTC

Problem Statement
In a multi‑module Maven project you often need a single executable artifact that contains all required libraries. Two common approaches are the maven-shade-plugin and the maven-assembly-plugin. Choosing between them, and ensuring that every module uses the same library versions, is a frequent engineering decision.
The key constraints are:
- Deterministic, reproducible builds.
- Minimal runtime classpath complexity.
- Ease of upgrading library versions across modules.
- Avoiding resource duplication or missing optional modules.
Decision Framework
Below is a compact table that compares the two plugins and highlights how Maven’s dependency mediation and <dependencyManagement> influence the outcome.
| Aspect | Shade Plugin | Assembly Plugin |
|---|---|---|
| Artifact Shape | Single JAR (fat JAR) | Archive (ZIP/JAR) containing original JARs and a manifest classpath |
| Classpath Handling | All classes merged into one file; no external classpath needed | Requires runtime classpath resolution (e.g., wrapper script or Class-Path in manifest) |
| Dependency Conflicts | Shaded dependencies are unified; optional relocation can avoid clashes | Original JARs kept; conflicts remain at runtime if classpaths overlap |
| Resource Duplication | Possible if not filtered; must configure filters | Resources preserved as separate files; less risk of accidental merge |
| Build Reproducibility | High when filters are explicit; deterministic artifact content | Depends on descriptorRefs and external JAR ordering |
| Upgrade Simplicity | Centralized <dependencyManagement> controls versions; shading uses those versions | Same; but runtime classpath may need manual updates |
| Runtime Failures | Risk of missing optional modules if not included | Risk of classpath ordering or missing JARs in container environments |
| Use Case | Standalone executables, Docker images, OSGi bundles | ZIP bundles for deployment, multi‑module installers, or when you need to preserve separate JARs |
Understanding Maven’s Dependency Mediation
Maven resolves conflicting transitive dependencies by selecting the nearest definition in the dependency graph. For example, if module A depends on commons-lang3:3.9 and module B depends on commons-lang3:3.8, and both are pulled transitively into the root project, Maven will use 3.9 because it is closer to the root in the graph. This deterministic rule ensures a single version of each artifact at build time.
When you add <dependencyManagement> to the root pom, you can override these selections and lock all modules to a particular version, preventing accidental drift.
Concrete Implementation: Shade Plugin Example
Below is a minimal pom.xml fragment that demonstrates a shade configuration with relocation to avoid class conflicts. Run this from the project root with mvn clean package.
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>app</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>org.slf4j</pattern>
<shadedPattern>shade.org.slf4j</shadedPattern>
</relocation>
</relocations>
<filters>
<filter>
<artifact>org.apache.logging.log4j:log4j-core</artifact>
<excludes>
<exclude>**/log4j2.xml</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.24.0</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Verification steps:
- Run
mvn clean packageasmvnuser withreadandwritepermissions totarget/. - Check the resulting JAR:
jar tf target/app-1.0.0-shaded.jar | grep com/exampleshould list your classes. - Test execution:
java -jar target/app-1.0.0-shaded.jarshould start without class‑not‑found errors.
Concrete Implementation: Assembly Plugin Example
This fragment creates a ZIP bundle that contains the original JAR and all runtime dependencies. The manifest includes a Class-Path entry pointing to the bundled JARs.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>com.example.Main</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
Verification steps:
- Run
mvn clean package. - Inspect the ZIP:
unzip -l target/app-1.0.0-jar-with-dependencies.zipshould list all dependency JARs. - Check the manifest:
jar xf target/app-1.0.0-jar-with-dependencies.jar META-INF/MANIFEST.MFand confirm theClass-Pathline includes the bundled JAR names. - Execute with classpath:
java -jar target/app-1.0.0-jar-with-dependencies.jarshould run successfully.
Centralizing Versions with <dependencyManagement>
Place a <dependencyManagement> section in the root pom.xml and declare the desired versions for libraries that appear in multiple modules. Each child module then simply references the artifact without specifying a version. This guarantees that all modules pull the same version and that Maven’s mediation rule will honor the root declaration.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.15.0</version>
</dependency>
</dependencies>
</dependencyManagement>
Child module example:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<scope>compile</scope>
</dependency>
Verification: run mvn help:effective-pom in any module and confirm the commons-lang3 version is 3.15.0.
Trade‑Off Summary
- Shade gives you a single executable file, simplifying deployment but requires careful relocation and filtering to avoid resource duplication. It’s ideal for micro‑services or when you need a self‑contained JAR.
- Assembly preserves the original JARs, making it easier to debug and to keep optional modules. However, you must manage the classpath at runtime, which can be more complex in containerized environments.
- Using
<dependencyManagement>eliminates version drift and makes upgrades predictable, regardless of the packaging strategy.
Practical Checklist Before Commit
- Confirm that all modules reference the root
dependencyManagement(no explicit versions). - If using Shade, verify that relocated packages do not clash with your own code.
- Run
mvn clean verifyto ensure tests pass on the shaded or assembled artifact. - Use
jar tforunzip -lto inspect the final artifact contents. - Deploy the artifact to a staging environment and run integration tests.
By following this decision guide, you can choose the packaging strategy that aligns with your deployment model and maintain a consistent, upgradeable dependency set across your Maven project.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.