What is the difference between a fat/uber JAR and Spring Boot's layered JAR support?
Quick Answer
A fat (or uber) JAR packages an application's own compiled classes together with every one of its dependencies' classes into a single, self-contained, runnable JAR — simple to deploy, but the entire JAR is treated as one monolithic layer by tools like Docker, so any code change invalidates and re-uploads the whole thing. Spring Boot's layered JAR support splits that same fat JAR into separate layers (dependencies, Spring Boot loader, resources, application classes) ordered by how often each changes, so a container build tool can cache the rarely-changing dependency layers and only rebuild/re-push the thin, fast-changing application-code layer.
Detailed Answer
A traditional fat JAR (also called an "uber JAR") bundles an application's own compiled .class files together with the compiled classes of every dependency into one single, self-executable JAR (java -jar app.jar works with no separate classpath setup) — this is what Spring Boot's default bootJar/spring-boot:repackage build step produces.
The problem for containerized deployment: if you naively COPY app.jar into a Docker image, that JAR is a single, opaque blob — Docker's layer caching can't distinguish "my dependencies, which rarely change" from "my own application code, which changes on every commit." Every code change forces re-uploading/re-pulling the entire JAR (dependencies included) as a new image layer.
Layered JAR support addresses this by splitting the same overall content into separate, logically-grouped layers within the JAR, ordered from least-frequently-changing to most-frequently-changing:
dependencies/ (third-party libraries — changes rarely)
spring-boot-loader/ (Spring Boot's own launcher classes — changes almost never)
snapshot-dependencies/ (SNAPSHOT deps — change a bit more often)
application/ (your own compiled classes and resources — changes on every build)
A multi-stage Dockerfile can then COPY each layer separately:
FROM eclipse-temurin:21-jre AS builder
COPY app.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract
FROM eclipse-temurin:21-jre
COPY --from=builder dependencies/ ./
COPY --from=builder spring-boot-loader/ ./
COPY --from=builder snapshot-dependencies/ ./
COPY --from=builder application/ ./ # this layer changes almost every build
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
Because Docker caches image layers and only rebuilds layers whose inputs changed, this structure means a typical code-only change only needs to rebuild and re-push the small application/ layer — the (usually much larger) dependencies/ layer stays cached across builds, dramatically speeding up image builds and reducing registry push/pull sizes in CI/CD pipelines.