skip to content

What does minimize() do in a Shadow build, and what risk does it introduce?

level: middleimportance: should knowfreq 45%

answer

  1. reachability analysis prunes unused classes
  2. shrinks fat jar
  3. breaks reflection / ServiceLoader / DI
  4. minimize { exclude(dependency(...)) }
  5. test the shaded jar

basics

~10 s

minimize() strips classes from bundled dependencies that aren't reachable from your code, shrinking the fat jar. The risk: it can remove classes only used reflectively or via service loading, breaking the app at runtime.

solid answer

~50 s

`minimize()` runs a **reachability analysis**: starting from your project's own classes, Shadow walks static references to determine which dependency classes are actually used, then **drops the rest** from the fat jar to reduce size. It's effective when you pull in a big library but touch only a sliver of it. The danger is that static analysis can't see **dynamic** usage — classes loaded by reflection (`Class.forName`), `ServiceLoader` providers, dependency-injection wiring, or string-driven instantiation appear unreferenced and get pruned, causing `ClassNotFoundException` / `NoClassDefFoundError` at runtime. Shadow lets you protect dependencies you know are needed dynamically: `minimize { exclude(dependency("group:artifact:.*")) }` keeps them whole. Practically you minimize, then test the produced jar end-to-end (especially reflection-heavy frameworks like Spring, Jackson, JDBC) and add excludes for anything that breaks. Minimization is orthogonal to relocation and service-file merging and is typically a last optimization step.

code

kotlin · 6 lines
kotlin
shadowJar {
    minimize {
        // keep reflection/DI-heavy deps whole
        exclude(dependency("org.springframework:.*:.*"))
    }
}

go deeper

for a junior

Know minimize() shrinks the jar by dropping unused dependency classes.

for a middle

Explain the reachability analysis and the reflection/ServiceLoader/DI pruning risk plus the exclude(dependency(...)) mitigation.

for a senior

Describe the test-the-shaded-jar workflow and interaction with mergeServiceFiles and reproducibility.

for a principal

Decide when size savings justify the analysis-fragility risk and codify exclude policies and validation gates.

## Why minimize Fat jars are big because they bundle whole dependencies even if you call one class from each. `minimize()` trims that fat by removing **unreachable** classes — classes no path from your code statically references. ## How it works Shadow builds a dependency graph of class references starting from your module's compiled classes (the *roots*). It follows method calls, field types, supertypes, annotations, etc. Any dependency class **not reachable** from a root is considered dead and excluded from the output jar. ```kotlin shadowJar { minimize() } ``` ## The fundamental limitation: dynamic usage Static reachability cannot see code paths the JVM resolves at runtime: - **Reflection**: `Class.forName("com.x.Impl")` — the name is a string, invisible to the analyzer. - **ServiceLoader / SPI**: providers listed in `META-INF/services/*` are instantiated reflectively. - **DI containers**: Spring/Guice/CDI instantiate beans by name or annotation scanning. - **Deserialization**: Jackson/Gson create types named only in JSON or config. - **Resource-driven plugins**: classes named in properties or config files. These classes look unused and get pruned, so the app fails at runtime with `ClassNotFoundException` or `NoClassDefFoundError` — and only on the code path that needs them, so it can slip past shallow tests. ## Protecting dependencies You can exclude specific dependencies from minimization so they're kept whole: ```kotlin shadowJar { minimize { exclude(dependency("org.springframework:.*:.*")) exclude(dependency("com.fasterxml.jackson.core:.*:.*")) } } ``` The `dependency(...)` matcher takes a `group:artifact:version` regex. You can also keep specific classes/packages via project-level configuration. ## Workflow 1. Add `minimize()`. 2. Build and run the produced jar through real integration/smoke tests, not just unit tests. 3. For any `ClassNotFoundException`, add an `exclude(dependency(...))` for the offending library. 4. Re-test; commit the excludes as documented exceptions. ## Interaction with other features - It complements `mergeServiceFiles()`: keep the service files **and** their provider classes (exclude reflection-loaded providers from minimization). - It's independent of `relocate()`; you can do both. - Reproducibility: pin plugin versions, since minimization output depends on Shadow's analyzer. ## Bottom line Minimize is a size optimization that trades safety for bytes. Use it when size matters, always validate the shaded jar against runtime behavior, and explicitly protect reflective/service-loaded dependencies.

  • Why can minimize() break a reflection-heavy app?
    Reflective loads (`Class.forName`, ServiceLoader, DI bean scanning, deserialization) reference classes only by string/metadata, so the static reachability analysis treats them as dead and prunes them, causing ClassNotFoundException at runtime.
  • How do you keep a known-needed dependency from being stripped?
    Use `minimize { exclude(dependency("group:artifact:.*")) }` with a regex matcher so that dependency's classes are retained whole.
  • What kind of testing must accompany minimize()?
    End-to-end/integration smoke tests against the actual produced fat jar that exercise reflective and service-loaded code paths — unit tests run against the unminimized classpath won't catch the missing classes.

Minimize is like packing only the clothes you've actually worn this week — efficient, until the one formal outfit you keep 'just in case' (loaded by reflection) is exactly what you need and you left it behind.

saying these in an interview costs you the question

  • Claiming minimize() is safe to enable blindly on any project.
  • Saying it removes duplicate classes (that's a collision concern, not reachability).
  • Forgetting that reflection/ServiceLoader usage is invisible to it.

context