skip to content

Layered jars

Layered jars split the archive into dependency, loader, snapshot and application layers so a Docker image only rebuilds the layer that changed. A practical answer whenever an interview turns to image size and build time.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

5

What are the four default layers in a Spring Boot layered jar, and why are they ordered the way they are?

level: middleimportance: must knowfreq 55%

answer

  1. dep -> loader -> snapshot -> application
  2. stable at bottom, volatile at top
  3. invalidation cascades upward in Docker
  4. layerOrder mirrors change frequency
  5. loader is separate because it's at jar root

basics

~10 s

In order: dependencies, spring-boot-loader, snapshot-dependencies, application. They go from least-likely-to-change to most-likely-to-change so Docker keeps the stable ones cached and only rebuilds the top application layer.

solid answer

~40 s

The defaults, ordered stable-to-volatile, are: (1) `dependencies` — released third-party jars; (2) `spring-boot-loader` — the `org/springframework/boot/loader` launcher classes, which change only on a Boot upgrade; (3) `snapshot-dependencies` — SNAPSHOT libraries, which change more often than releases; (4) `application` — your own classes and resources, which change on every build. The ordering is deliberate: Docker stacks image layers and invalidates every layer above a changed one, so the things that rarely change are placed first (bottom) and stay cached, while your volatile application code sits last (top). Only that small top layer is re-stored and re-pushed on a routine code change. The mapping of files to layers lives in `BOOT-INF/layers.idx`, and you can customize both the layer set and the order in the Gradle/Maven plugin config if your change-frequency profile differs.

code

kotlin · 24 lines
kotlin
// build.gradle.kts — customizing layers to add a 'company-dependencies' layer
// for your own fast-changing internal libs, between third-party deps and app code.
tasks.named<org.springframework.boot.gradle.tasks.bundling.BootJar>("bootJar") {
    layered {
        dependencies {
            intoLayer("snapshot-dependencies") { include("*:*:*SNAPSHOT") }
            intoLayer("company-dependencies") { include("com.mycompany:*") }
            intoLayer("dependencies")
        }
        application {
            intoLayer("spring-boot-loader") { include("org/springframework/boot/loader/**") }
            intoLayer("application")
        }
        layerOrder.set(
            listOf(
                "dependencies",
                "spring-boot-loader",
                "snapshot-dependencies",
                "company-dependencies",
                "application",
            ),
        )
    }
}

go deeper

for a junior

Just remember the four names and that stable comes first, code last.

for a middle

Explain the change-frequency rationale and Docker's upward cascade invalidation.

for a senior

Discuss layers.idx as an ordered index and customizing layers via the plugin for internal libs.

for a principal

Design a layering scheme matched to a monorepo's actual change cadence and CI cache topology; weigh layer count vs. cache granularity.

### The four default layers (order is the answer) Spring Boot's build plugins define, by default, four layers listed from **least** to **most** frequently changing: | Order | Layer | Contents | Change frequency | |-------|-------|----------|------------------| | 1 | **`dependencies`** | Released (non-SNAPSHOT) third-party jars under `BOOT-INF/lib/` | Very low — only on a dependency bump | | 2 | **`spring-boot-loader`** | The `org/springframework/boot/loader/**` classes at the jar root (the launcher that makes `java -jar` work) | Very low — only on a Spring Boot version change | | 3 | **`snapshot-dependencies`** | SNAPSHOT dependencies (e.g. `1.0.0-SNAPSHOT`) | Medium — SNAPSHOTs are volatile by nature | | 4 | **`application`** | Your code and resources: `BOOT-INF/classes/**`, `META-INF/**` | High — every build | ### Why this exact order A Docker image is a stack of layers; when a layer's content changes, that layer **and every layer created after it** are invalidated and must be rebuilt/re-pushed. Therefore you want the immutable stuff at the bottom and the churny stuff at the top. Dependencies (huge, stable) go first so they remain a cache hit across nearly all builds; your application code (tiny, changes constantly) goes last so its invalidation costs almost nothing. Placing `snapshot-dependencies` *above* released `dependencies` reflects that SNAPSHOTs change more often than releases but still less often than your own code. ### `layers.idx` The file `BOOT-INF/layers.idx` inside the jar is an **ordered** index mapping each layer name to the paths it owns. `layertools` reads it during extraction; the order in the file *is* the intended Docker copy order. ### Customizing layers Both plugins let you redefine layers and their content. Gradle example: ```groovy bootJar { layered { application { intoLayer("spring-boot-loader") { include("org/springframework/boot/loader/**") } intoLayer("application") } dependencies { intoLayer("snapshot-dependencies") { include("*:*:*SNAPSHOT") } intoLayer("company-dependencies") { include("com.mycompany:*") } intoLayer("dependencies") } layerOrder = ["dependencies", "spring-boot-loader", "snapshot-dependencies", "company-dependencies", "application"] } } ``` A common real-world refinement is splitting your own frequently-changing internal libraries into their own layer between third-party deps and application code. ### Gotchas - **Order in `layerOrder` must be consistent with real change frequency** — misordering (e.g. putting `application` early) defeats the caching benefit. - The `spring-boot-loader` layer exists because those launcher classes live at the jar *root*, not under `BOOT-INF/lib/`, so they need their own bucket. - Every path in the jar must map to exactly one layer; unmatched files cause a build error, which is why the default config ends with catch-all `intoLayer("application")` / `intoLayer("dependencies")` rules. - Customization only affects packaging/caching — never runtime behavior or classpath semantics.

  • Why is spring-boot-loader its own layer instead of part of dependencies?
    Those launcher classes live at the jar root (outside BOOT-INF/lib), and they change only on a Spring Boot version bump — a different, very-low change cadence — so they get their own bucket for clean caching.
  • How would you carve out your own frequently-changing internal libraries?
    Define a custom layer (e.g. company-dependencies) via intoLayer with a group filter, and place it in layerOrder above application but below third-party dependencies, so a released third-party bump still doesn't invalidate app code.

saying these in an interview costs you the question

  • Listing the layers in the wrong order (e.g. application first)
  • Claiming layering compresses or shrinks the jar
  • Saying you cannot customize the layers

context

open as a page

How does `java -Djarmode=layertools extract` work, and how do you wire it into a multi-stage Dockerfile?

level: seniorimportance: must knowfreq 50%

basics

~20 s

java -Djarmode=layertools -jar app.jar extract reads layers.idx and writes one folder per layer. In a builder Docker stage you run it, then COPY each folder into the final image separately (stable first) and start with JarLauncher.

open as a page

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

level: juniorimportance: should knowfreq 45%

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.

open as a page

How do you enable/verify layered-jar packaging in the Spring Boot build plugin, and how can you inspect a jar's layers?

level: middleimportance: should knowfreq 35%

basics

~20 s

Layering is on by default since Spring Boot 2.4 via the Gradle/Maven Boot plugin. You can toggle it in the plugin's layered config. To inspect, run java -Djarmode=layertools -jar app.jar list, or unzip and open BOOT-INF/layers.idx.

open as a page

When would you choose the layertools Dockerfile approach over buildpacks (bootBuildImage), and how does layering relate to newer optimizations like CDS/AOT?

level: principalimportance: nice to knowfreq 25%

basics

~20 s

Use the layertools Dockerfile when you need full control over the base image, JVM flags, or non-standard layout. Use buildpacks (bootBuildImage) for zero-Dockerfile, best-practice images. Layering speeds up builds/pushes; CDS/AOT speed up app startup — they are complementary, not alternatives.

open as a page