When you build a 'fat jar' (also called an uber jar) for a Java application, what actually goes into that archive, and why is it needed compared to a normal 'thin' application jar?
answer
- thin jar needs external classpath
- Shade/Shadow plugin
- Spring Boot BOOT-INF nested jars
- META-INF/services merge conflict
- flat merge overwrites classes
basics
~20 sA fat jar packs your compiled code together with the compiled code of every library it depends on into one single file, so you can run it anywhere with just java -jar, no separate downloads needed.
solid answer
~50 sA thin jar contains only your own compiled classes plus a manifest listing which dependency jars must be on the classpath at runtime — those other jars must be supplied separately (an app server's shared lib folder, etc.). A fat/uber jar instead unpacks every transitive dependency's classes and resources and repackages them all into one archive, usually via a build-tool plugin (Maven Shade Plugin, Gradle Shadow plugin, Spring Boot's repackage). This produces a single self-contained artifact you can deploy and run with `java -jar app.jar` on any machine with a JVM — exactly what you want for containerized microservices, CLI tools, and Spring Boot apps. The costs are a larger artifact, merge conflicts when two dependencies ship a file at the same path (e.g. META-INF/services entries), and classpath collisions if two dependencies embed different versions of the same third library — which is what shading/relocation exists to fix.
go deeper
Should know a fat jar bundles dependencies so the app runs standalone with java -jar, versus a thin jar which doesn't.
Should name at least one real build-tool mechanism (Shade, Shadow, Spring Boot repackage) and one concrete cost (size or merge conflict).
Should explain the META-INF/services merge-conflict failure mode and how transformer/merge-strategy configuration addresses it.
Should articulate the library-vs-application distinction — why applications fat-jar but well-behaved libraries don't — and connect classpath collisions to the broader shading solution.
## The thin jar A normal Java build produces what's called a **'thin' jar**: an archive containing only the `.class` files and resources your own module compiled, plus a `META-INF/MANIFEST.MF` file that can declare a `Class-Path` entry pointing at other jars the application needs at runtime. That thin jar is not runnable on its own — if you try `java -jar myapp.jar` and your code imports Jackson or Guava, the JVM throws `NoClassDefFoundError` the moment it tries to load a class from those libraries, because they simply aren't inside the archive. Historically this was fine because deployment environments (an application server like Tomcat or WebLogic, or a shared `/lib` directory on a server) provided the transitive dependencies separately, and the thin jar just needed to know their names and versions. ## What a fat jar actually contains A **fat jar**, also called an **uber jar**, solves the 'I need one artifact I can copy anywhere and run' problem by physically unpacking every dependency jar in the transitive closure and merging their contents — `.class` files, resource files, `META-INF` metadata — into a single output archive alongside your own compiled code. A build-tool plugin does this: - The **Maven Shade Plugin** and the **Gradle Shadow plugin** both walk the resolved dependency graph, extract each jar, and repackage everything into one file with a manifest whose `Main-Class` entry points at your application's entry point. - **Spring Boot** takes a slightly different approach with its 'repackage' step — rather than flatly merging classes, it nests the original dependency jars inside a `BOOT-INF/lib/` folder inside the fat jar and uses a custom class loader (`JarLauncher`) at startup that knows how to load classes out of those nested jars, avoiding some of the flat-merge collision problems described below while still producing one self-contained, `java -jar-runnable` file. ## Why it matters operationally The reason this matters operationally is deployment simplicity and reproducibility. A single self-contained jar is exactly what you want to hand to a container image (`COPY app.jar /app.jar`, `ENTRYPOINT ["java","-jar","/app.jar"]`), to a CLI tool users download and run, or to any environment where you can't guarantee a pre-populated classpath. It also pins the exact dependency versions resolved at build time into the shipped artifact, so there's no risk of the runtime environment silently supplying a different, incompatible version of a shared library than the one the app was built and tested against. ## The three failure modes That guarantee comes at real cost, showing up in three failure modes. 1. **First, size:** an uber jar for a moderately complex Spring Boot service can easily reach tens of megabytes, because it now embeds the code of every transitive dependency rather than referencing it — multiplied across many microservices each shipping its own private copy of, say, Jackson, this is meaningfully wasteful compared to a shared classpath. 2. **Second, resource merging conflicts:** several libraries can legitimately ship a file at the exact same path inside their jar — the classic case is `META-INF/services/*` files used by Java's `ServiceLoader` mechanism, where two different libraries each contribute a line to what's supposed to be one merged file. A naive flat merge that overwrites on collision will silently drop one library's service registration, producing a runtime failure ('no implementation found for X') that's maddening to debug because it only appears in the packaged jar, never in the IDE where dependencies sit on separate classpath entries. Both Shade and Shadow plugins support explicit 'transformers'/merge strategies (e.g., `ServicesResourceTransformer`) specifically to concatenate rather than overwrite these files correctly. 3. **Third, and most seriously, classpath collisions on classes themselves:** if two different dependencies in the tree pull in two different versions of the same third library (a very common 'diamond dependency' situation), a flat-merge fat jar has no way to keep both; whichever gets copied last silently wins, and code compiled against the other version can fail at runtime with `NoSuchMethodError` or `ClassNotFoundException`, again only inside the packaged artifact. This exact problem is what class **shading/relocation** (renaming a library's packages, e.g. `com.google.guava` to `myapp.shaded.com.google.guava`) exists to solve, by letting two different versions of the same library coexist inside one fat jar under different package names. ## Applications versus libraries In practice, teams building deployable services (Spring Boot apps, most containerized JVM microservices) default to fat jars because the deployment-simplicity win outweighs the size cost, while teams publishing libraries for other developers to consume deliberately avoid shading their own published artifact into a fat jar, since that would force every downstream consumer to inherit whatever transitive versions were baked in at publish time, defeating normal dependency resolution entirely.
- What specifically goes wrong when two merged dependencies both ship a file at the same path inside the fat jar?A naive flat merge just copies files in dependency order, so whichever jar is processed last silently overwrites the earlier one's file at that path. For files like Java's ServiceLoader META-INF/services entries, which are meant to be additive across libraries, this drops registrations and causes a 'no implementation found' failure that only reproduces in the packaged jar.
- Why does Spring Boot's repackaging approach differ from a straight Shade-style flat merge?Instead of unpacking and merging every dependency's classes into one flat namespace, Spring Boot nests the original dependency jars intact inside BOOT-INF/lib/ and ships a custom launcher class loader that reads classes directly out of those nested jars at startup. That sidesteps most flat-merge collision problems since each dependency's files stay isolated in their own nested archive.
- Why would a library author deliberately avoid publishing their own artifact as a fat jar?If a library bakes its own transitive dependencies into its published jar, every consumer inherits those exact versions regardless of what else is on their classpath, which breaks normal Maven/Gradle dependency resolution and can force consumers into version conflicts they have no way to resolve themselves. Libraries are expected to publish thin, with dependencies declared in their POM/module metadata so consumers' build tools can negotiate versions.
A thin jar is like shipping a recipe card that lists ingredients you're expected to already have in your pantry; a fat jar is a meal kit that includes every ingredient pre-packed in the box, so it works anywhere but weighs a lot more to ship.
saying these in an interview costs you the question
- Thinks a fat jar and a regular jar are the same size/content
- Doesn't know dependencies must physically be unpacked/merged in
- Unaware that merge conflicts on identical file paths are possible
- Recommends shading a public library artifact without caveats
- Can't explain why java -jar fails on a thin jar with missing deps