skip to content

Contrast Spring Boot's nested-jar fat jar with a shaded/uber jar. What problems does nesting avoid?

level: seniorimportance: should knowfreq 35%

answer

  1. shade = merge/explode → path collisions
  2. collisions: spring.factories, META-INF/services, .imports
  3. nested = whole jars, nothing overwritten
  4. nesting enables layered jars + preserves signing
  5. can't use Boot fat jar as a library dep

basics

~20 s

A shaded jar unzips every dependency and merges all classes into one flat jar, which can overwrite duplicate resources. Spring Boot keeps each dependency as a whole nested jar under BOOT-INF/lib, preserving resources and library identity.

solid answer

~40 s

A shaded/uber jar (Maven Shade, Gradle Shadow) explodes each dependency and merges every class and resource at the jar root. The hazard is **path collisions**: many libraries ship files at the same path — `META-INF/spring.factories`, `META-INF/services/*` (ServiceLoader), `META-INF/spring/*.imports`, signature files — and a naive merge lets one clobber another, silently breaking auto-configuration or service discovery. Shade mitigates with transformers (e.g., ServicesResourceTransformer, AppendingTransformer) and relocation, but that's manual and error-prone. Spring Boot instead keeps each dependency **whole and nested** under BOOT-INF/lib, so no resource is ever merged or overwritten — each jar retains its own metadata. The cost is that a custom launcher (JarLauncher) and classloader are required to read jar-in-jar, and you must use `java -jar`, not `-cp`. Nesting also preserves jar signing and makes layered/exploded builds and reproducible ordering (classpath.idx) straightforward.

code

text · 14 lines
text
# Shade must MERGE colliding resources or auto-config breaks:
<transformers>
  <!-- combine all META-INF/services/* instead of overwriting -->
  <transformer implementation=
    "org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
  <!-- append spring.factories rather than dropping duplicates -->
  <transformer implementation=
    "org.apache.maven.plugins.shade.resource.AppendingTransformer">
    <resource>META-INF/spring.factories</resource>
  </transformer>
</transformers>

# Spring Boot needs NONE of this: each dependency jar stays whole under
# BOOT-INF/lib, so its META-INF/services and spring.factories are never merged.

go deeper

for a junior

Know shaded merges classes, Boot nests whole jars — different approaches.

for a middle

Name a concrete collision (META-INF/services or spring.factories) that nesting avoids.

for a senior

Discuss transformers/relocation trade-offs, the classloader cost, and that Boot jars aren't usable as deps.

for a principal

Connect nesting to layered images, reproducible builds, signing preservation, and artifact strategy.

## Two ways to build a single runnable jar ### 1. Shaded / uber jar (merge model) Tools: **Maven Shade plugin**, **Gradle Shadow plugin**. They take every dependency, **unzip it**, and copy all its `.class` files and resources into **one flat output jar** rooted at the top. The classpath is then trivial — everything is at the root, loaded by the ordinary classloader. **The core hazard — path collisions.** Java resources are addressed by path, and multiple libraries legitimately ship files at the *same* path: - `META-INF/spring.factories` (legacy auto-config registration) - `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` (Boot 2.7+/3.x auto-config) - `META-INF/services/<interface>` (JDK **ServiceLoader** / SPI) - `reference.conf` (Typesafe Config, used by Akka) - `module-info.class`, digest/signature files under `META-INF/` A naive merge keeps only the **last** file written at a path, silently dropping the others. Result: missing auto-configurations, broken SPI providers, corrupted config. Shade addresses this with **transformers** that *combine* rather than overwrite (e.g., `ServicesResourceTransformer`, `AppendingTransformer`, `ManifestResourceTransformer`) and with **relocation** (rewriting package names to dodge class-level conflicts). All of this is **manual configuration** you must get right per-project. ### 2. Spring Boot nested jar (preserve model) Spring Boot **does not merge**. Each dependency stays a **complete, untouched jar** under `BOOT-INF/lib`, and your code sits under `BOOT-INF/classes`. Because nothing is merged: - No resource is ever overwritten — every library keeps its own `spring.factories`, `META-INF/services`, etc. - Library **identity** and even **jar signatures** are preserved. - Ordering is explicit and reproducible via **classpath.idx**. **The cost:** the ordinary classloader can't read a jar inside a jar, so Spring Boot ships a **custom launcher (JarLauncher)** and **LaunchedClassLoader**. You must run with `java -jar` (the launcher), not `java -cp`. Historically this also meant a custom `jar:` URL handler. ## Trade-off summary | Aspect | Shaded/uber | Spring Boot nested | |---|---|---| | Dependency form | Exploded, classes merged | Whole jars nested | | Resource collisions | Must handle via transformers | Impossible (never merged) | | Classloading | Standard | Custom LaunchedClassLoader | | Launch | `java -cp`/`-jar` any main | `java -jar` via JarLauncher | | Jar signatures | Broken by merge | Preserved | | Layered/exploded builds | Awkward | First-class (layertools) | | Startup classloading cost | Minimal | Slight overhead reading nested jars | ## When each fits - **Spring Boot nested** — the default for Boot apps; correctness by construction, plays with layered images and exploded-jar container startup. - **Shaded** — libraries or CLI tools shipped as a single dependency-free artifact that others put *on their classpath* (you can't nest a Boot fat jar as a normal dependency), or non-Boot apps. Requires careful transformer setup when Spring/SPI resources are present. ## Interview-worthy nuances - **You cannot use a Spring Boot fat jar as a library dependency** of another project — its nested layout isn't a normal classpath jar. If you need that, build a plain (thin) jar too; the plugin keeps the plain jar (classifier) or you configure it. - **Layered jars** (`Spring-Boot-Layers-Index`, `layertools`) exist *because* dependencies stay whole — you can split BOOT-INF/lib into cache-friendly Docker layers (dependencies change rarely, app code often). A shaded jar can't do this cleanly. - **Reproducible builds** — nesting whole jars plus classpath.idx makes ordering deterministic.

  • Can you add a Spring Boot fat jar as a Maven/Gradle dependency of another module?
    No — the nested BOOT-INF layout isn't a normal classpath jar. You'd build/publish the plain (thin) jar instead. The Boot plugin can keep the plain jar (e.g., a classifier) alongside the executable one.
  • How does the nested layout enable layered Docker images?
    Because dependencies stay whole under BOOT-INF/lib, layertools/the layers index can split them into separate Docker layers (rarely-changing deps vs frequently-changing app code), maximizing image-layer cache hits. A merged shaded jar can't be split cleanly.

saying these in an interview costs you the question

  • Claiming Spring Boot's fat jar is the same as a shaded/uber jar.
  • Saying you can just add a Boot fat jar as a normal library dependency.
  • Ignoring resource-collision risks (spring.factories, META-INF/services) when shading Spring apps.

context