skip to content

What is a Spring Boot layered jar and why does it help with Docker image builds?

level: juniorimportance: should knowfreq 45%

answer

  1. fat jar = one big COPY, cache-busts everything
  2. layers ordered stable -> volatile
  3. dependencies / loader / snapshot / application
  4. layers.idx records the mapping
  5. COPY each layer separately -> cache hits

basics

~20 s

A layered jar splits a Spring Boot fat jar into groups (layers) that change at different rates. In a Dockerfile you copy each layer separately so slow-changing dependency layers stay cached and only your fast-changing code is rebuilt.

solid answer

~40 s

A Spring Boot fat (uber) jar normally bundles your compiled classes plus all dependencies in one archive. If you `COPY` that single jar into a Docker image, any one-line code change invalidates the whole layer and Docker re-ships every dependency. A layered jar reorganizes the same contents into ordered layers based on how often they change — `dependencies`, `spring-boot-loader`, `snapshot-dependencies`, and `application` (your code, last). A `BOOT-INF/layers.idx` file records which files belong to which layer. In a multi-stage Dockerfile you extract the layers and `COPY` each into its own image layer. Because your third-party dependencies rarely change, Docker reuses those cached image layers across builds and only rebuilds the small `application` layer. Result: faster builds, smaller pushes/pulls, better registry cache hits. It is enabled by default in Spring Boot 2.4+.

code

java · 15 lines
java
// Not runtime code — a multi-stage Dockerfile using layered-jar extraction.
// (java-fenced for syntax clarity; this is Docker build config.)

// FROM eclipse-temurin:21-jre AS builder
// WORKDIR /app
// COPY target/app.jar app.jar
// RUN java -Djarmode=layertools -jar app.jar extract
//
// FROM eclipse-temurin:21-jre
// WORKDIR /app
// COPY --from=builder /app/dependencies/ ./
// COPY --from=builder /app/spring-boot-loader/ ./
// COPY --from=builder /app/snapshot-dependencies/ ./
// COPY --from=builder /app/application/ ./
// ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

go deeper

for a junior

Know the one-sentence why: split the jar so Docker caches unchanging dependencies and only re-ships your code.

for a middle

Name the four default layers in order and connect them to Docker layer caching.

for a senior

Explain layers.idx, layertools extraction, and the multi-stage Dockerfile pattern with JarLauncher entrypoint.

for a principal

Compare against buildpacks/bootBuildImage; reason about registry storage, CI cache strategy, and when custom layer definitions pay off.

### Background: fat jars and Docker layers Spring Boot packages an application as an **executable fat jar** (also called an uber jar): a single `.jar` containing your compiled classes under `BOOT-INF/classes/`, all third-party dependency jars under `BOOT-INF/lib/`, and Spring Boot's own launcher classes (`org.springframework.boot.loader.*`) at the root so `java -jar app.jar` can boot. A **Docker image** is built from a stack of read-only **layers**. Each Dockerfile instruction (`COPY`, `RUN`, …) produces one layer. Docker caches layers by content: if the inputs to an instruction are unchanged, the cached layer is reused instead of rebuilt/re-transferred. The naive Dockerfile does `COPY target/app.jar app.jar` then `ENTRYPOINT ["java","-jar","app.jar"]`. Because the whole fat jar is one file, changing a single line of your own code produces a new jar, which invalidates that `COPY` layer — so Docker must re-store and the registry must re-push *all* your dependencies too, even though they didn't change. For a typical app that is tens or hundreds of MB re-shipped for a trivial change. ### What a layered jar does Since **Spring Boot 2.3 (opt-in) / 2.4+ (default)**, the fat jar can be organized into **layers** — logical groups of the jar's contents ordered from least-likely-to-change to most-likely-to-change. The default layers, in order: 1. **`dependencies`** — released (non-SNAPSHOT) third-party dependencies. Change rarely. 2. **`spring-boot-loader`** — the `org/springframework/boot/loader/**` launcher classes. Change only on a Spring Boot version bump. 3. **`snapshot-dependencies`** — SNAPSHOT dependencies, which change more often than releases. 4. **`application`** — *your* application classes and resources (`BOOT-INF/classes/**`, `META-INF/**`). Change on every build. Order matters: Docker layers are stacked bottom-up, so the stable layers go first (cached) and your volatile `application` layer goes last. ### `layers.idx` The mapping is recorded in **`BOOT-INF/layers.idx`** inside the jar — a YAML-like ordered list mapping each layer name to the file/directory paths it contains. `layertools` reads this index to know how to split the jar. Example: ``` - "dependencies": - "BOOT-INF/lib/spring-core-6.x.jar" - "spring-boot-loader": - "org/" - "snapshot-dependencies": - "application": - "BOOT-INF/classes/" - "META-INF/" ``` ### Extracting layers: `layertools` The jar ships with a **`layertools` jarmode**. Running `java -Djarmode=layertools -jar app.jar extract` reads `layers.idx` and writes one directory per layer (`dependencies/`, `spring-boot-loader/`, `snapshot-dependencies/`, `application/`), each containing the runnable slice of the jar. `list` prints the layer names; `--destination` (or, in newer versions, `--launcher`/`extract` variants) controls output. In a **multi-stage Dockerfile**, a `builder` stage runs the extract, then the final stage `COPY --from=builder`s each layer directory into its own image layer, most-stable first. The container is started not with `java -jar` but with the extracted **launcher** (`org.springframework.boot.loader.launch.JarLauncher`) over the classpath dirs. ### When to use / not use - Use it whenever you containerize a Spring Boot app and care about build speed, push/pull time, and registry storage — i.e. almost always in CI/CD. - The competing approach is **Cloud Native Buildpacks** via `./gradlew bootBuildImage` / `./mvnw spring-boot:build-image`, which produces an optimized layered image *without you writing a Dockerfile at all* and also uses layering under the hood. - If you never rebuild frequently, the benefit is smaller — but there's essentially no downside; layering is on by default. ### Gotchas - The runtime **classpath ordering** is preserved via the launcher; don't try to `java -jar` an extracted directory — use the `JarLauncher` entrypoint. - Extraction requires the jar to actually be layered (default since 2.4, but a custom `layered { enabled = false }` in Gradle/Maven config disables it). - Only the *jar* changes; behavior, main class, and Spring context are identical — layering is purely a packaging optimization.

  • Why must the application layer be copied last in the Dockerfile?
    Docker caches layers top-down and invalidates every layer after the first change. The application layer changes on every build, so putting it last keeps the stable dependency layers above it cached and reusable.
  • Is layering enabled by default?
    Yes, since Spring Boot 2.4. In 2.3 it was opt-in. You can disable it via the plugin's layered configuration, but there's rarely a reason to.

context