How would you decide between a Shadow fat jar and an alternative packaging like Spring Boot's bootJar or a nested/exploded distribution, when standardizing build output across services?
answer
- flattened vs nested vs exploded
- bootJar preserves jar boundaries
- signed jars break when flattened
- resource collisions need transformers
- library = thin jar not fat
basics
~20 sShadow flattens dependencies into one classpath, which is great for plain JVM apps and CLIs but breaks libraries that depend on jar boundaries (signed jars, duplicate resources). For Spring apps prefer bootJar's nested layout. Choose per app type and resource-collision risk.
solid answer
~60 sShadow produces a **flattened** uber jar: all dependency classes/resources live in one namespace, so colliding resources must be merged with transformers and signed jars lose their signatures. That's ideal for plain JVM services, CLIs, and agents. **Spring Boot's `bootJar`** takes a different approach — a **nested** jar with a custom launcher (`org.springframework.boot.loader`) that keeps each dependency as its own jar inside `BOOT-INF/lib/`, preserving jar boundaries, resource isolation, and `spring.handlers`/`spring.factories` semantics automatically. For Spring apps, bootJar is almost always the right choice; using Shadow there means manually merging Spring's registry files. A third option is an **exploded/nested distribution** (Application/Distribution plugin `distZip` with a `lib/` of separate jars + start scripts) — no flattening, easy to diff and patch, slightly more files to ship. Standardization decision drivers: framework (Spring -> bootJar), whether jars are signed or have colliding resources (-> avoid flattening), artifact-size and startup constraints, reproducibility, and whether the artifact is a library vs an application. A pragmatic policy: bootJar for Spring services, Shadow for non-Spring apps/CLIs/agents, thin jar + metadata for libraries.
code
kotlin · 4 lines// Policy by app type
// Spring service -> bootJar (preserves spring.factories, signatures)
// Plain app/CLI -> shadowJar { mergeServiceFiles() }
// Library -> plain jar + publishing (no fat jar)go deeper
Know Shadow makes one flat jar; Spring Boot has its own bootJar; libraries shouldn't be fat-jarred.
Contrast flattened vs nested layouts and note resource-collision and signed-jar issues.
Drive a decision matrix (Spring->bootJar, app/CLI->Shadow, agent->Shadow+relocate, library->thin) with reproducibility/signing/resource reasoning.
Set an org-wide packaging standard and CI policy balancing framework fit, security (signatures), operability, and reproducibility.
## Three packaging shapes 1. **Flattened uber jar (Shadow)** — every dependency is unpacked and merged into one jar; one classloader, one namespace. 2. **Nested launcher jar (Spring Boot `bootJar`)** — dependencies stay as separate jars under `BOOT-INF/lib/`; a custom `LaunchedURLClassLoader` loads them. Jar boundaries and resource isolation are preserved. 3. **Exploded distribution (Application/Distribution `distZip`/`installDist`)** — your thin jar plus a `lib/` directory of individual dependency jars and generated start scripts, zipped/tarred. ## Why the shape matters ### Resource collisions Flattening forces all `META-INF/services/*`, `spring.factories`, `reference.conf` (Typesafe Config / Akka), `log4j2` plugin caches, etc. into single files. Shadow needs explicit transformers (`mergeServiceFiles()`, `append(...)`) for each. Nested/exploded layouts keep them in separate jars, so no merge is needed. ### Signed jars A flattened jar invalidates the original signatures (and you usually must strip `*.SF`/`*.RSA`/`*.DSA` to avoid `SecurityException`). Nested layouts preserve signatures. ### Framework expectations Spring Boot relies on its loader and `spring.factories`/auto-config discovery; `bootJar` wires this correctly. A Shadow jar of a Spring app works only if you replicate that merging manually — error-prone. ### Operational concerns - **Size**: all three are similar in total bytes; flattened has slightly less zip overhead, exploded ships many files. - **Patching/forensics**: exploded `lib/` lets you swap or inspect a single dependency jar; a flattened jar must be rebuilt. - **Startup**: nested loader has minor overhead vs flattened, usually negligible. - **Reproducibility**: pin plugin versions and set `isPreserveFileTimestamps`/`isReproducibleFileOrder` for stable hashes regardless of shape. ## A standardization policy | App type | Recommended packaging | |---|---| | Spring Boot service | `bootJar` | | Plain JVM service / CLI / Kotlin app | Shadow fat jar | | Java agent / build plugin / SDK loaded with others | Shadow with `relocate()` | | Published library | thin jar + POM/Gradle metadata (no fat jar) | | Apps needing OS scripts + patchable libs | Application/Distribution `distZip` | ## Decision checklist 1. Is it Spring Boot? -> bootJar. 2. Are jars signed or do dependencies have colliding/merge-sensitive resources you don't want to hand-merge? -> avoid flattening (nested/exploded). 3. Is it a library? -> don't fat-jar. 4. Does it need to coexist on a shared classpath? -> Shadow + relocation. 5. Otherwise, for a self-contained app -> Shadow fat jar. ## Example ```kotlin // non-Spring app: Shadow plugins { application; id("com.gradleup.shadow") version "8.3.0" } shadowJar { mergeServiceFiles(); minimize { exclude(dependency("com.zaxxer:HikariCP:.*")) } } ``` The core insight: Shadow trades jar-boundary semantics for a single artifact; pick it only when that trade-off is acceptable, and prefer a boundary-preserving layout (bootJar/dist) when resource isolation or signatures matter.
- Why can a Shadow fat jar break a signed dependency?Flattening merges the dependency's classes into a new jar whose contents no longer match the original signature manifests; the leftover `META-INF/*.SF/.RSA/.DSA` entries cause `SecurityException`, so they must be stripped, losing the signature guarantee.
- What does bootJar do differently that avoids most merge transformers?It keeps each dependency as a separate nested jar under `BOOT-INF/lib/` loaded by Spring's custom classloader, so resource files like `spring.factories` and `META-INF/services` stay isolated per jar and never collide.
- Why shouldn't a published library be a fat jar?It buries transitive dependencies inside the artifact, defeating consumer-side version resolution and conflict mediation and risking duplicate/incompatible classes; libraries should publish thin jars with dependency metadata.
Shadow blends every dependency into one smoothie; bootJar keeps the fruit in separate labeled containers inside one lunchbox; a distZip is a lunchbox with each item wrapped and a how-to-eat note (start script).
saying these in an interview costs you the question
- Recommending Shadow for Spring Boot apps without addressing spring.factories merging.
- Ignoring signed-jar invalidation when flattening.
- Treating fat jars as the universal packaging answer including for libraries.