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. It's 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. A container build tool can then 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"]
Docker caches image layers and only rebuilds layers whose inputs changed. So 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. This dramatically speeds up image builds and reduces registry push/pull sizes in CI/CD pipelines.