How does Spring Boot ensure DevTools never runs in production, and how do you declare the dependency correctly?
answer
- Gradle developmentOnly / Maven optional
- repackage excludes it from the fat jar
- self-disables when run from packaged jar
- restart CL only loads directory entries — jar has none
- force off: spring.devtools.restart.enabled=false (System prop)
basics
~20 sYou declare DevTools as developmentOnly (Gradle) or optional (Maven) so it isn't packaged into the production jar. As a safety net, DevTools also disables itself automatically when it detects the app was started from a fully packaged jar rather than exploded classes.
solid answer
~40 sThere are two independent safeguards. First, the **build scope**: in Gradle you use the `developmentOnly` configuration and in Maven you mark it `<optional>true</optional>`. This keeps DevTools off the transitive classpath of anything that depends on your app and, importantly, the Spring Boot repackage step excludes `developmentOnly`/`optional` DevTools from the fat jar. Second, **runtime self-disablement**: even if the jar were present, DevTools detects when the application is launched from a **fully packaged jar or war** (as opposed to exploded directory classes) and switches itself off — restart, LiveReload, and property defaults all go inactive. You can also force it off with `spring.devtools.restart.enabled=false` (best set as a System property so the watcher thread never starts). The net effect: correctly declared, DevTools simply isn't in the production artifact, and even a stray copy stays dormant.
code
kotlin · 8 lines// Gradle (Kotlin DSL) — correct dev-only declaration
dependencies {
developmentOnly("org.springframework.boot:spring-boot-devtools")
}
// Force-disable early, e.g. in a custom launcher, as a System property
// so the file-watcher thread is never started:
// System.setProperty("spring.devtools.restart.enabled", "false")go deeper
Know it's a dev-only dependency that isn't in the production jar.
Name developmentOnly/optional and the self-disable-from-jar behavior.
Explain how self-disable ties to the directory-only restart classloader and how to force-off.
Reason about exploded-run edge cases, remote-devtools risk, and defense-in-depth in packaging pipelines.
## Two layers of protection ### Layer 1 — Don't ship it (build configuration) The primary mechanism is dependency scope so DevTools never lands in the production artifact: - **Gradle**: use the `developmentOnly` configuration. ```kotlin dependencies { developmentOnly("org.springframework.boot:spring-boot-devtools") } ``` The Spring Boot Gradle plugin knows `developmentOnly` dependencies are for dev, and the `bootJar`/`bootWar` repackaging leaves them out of the archive. It's also not exposed to downstream consumers. - **Maven**: mark it `optional`. ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> ``` `optional` stops it being transitive. The Spring Boot Maven plugin additionally **excludes DevTools from the repackaged jar by default** (there was even a historical `excludeDevtools` flag; the modern default is to exclude it). ### Layer 2 — Self-disable at runtime DevTools is defensive even if it *does* end up on the classpath. It checks how the app was started: - Running from **exploded classes on disk** (directories on the classpath — typical of `mvn spring-boot:run`, `gradle bootRun`, or an IDE) → DevTools is **active**. - Running from a **fully packaged jar/war** (`java -jar app.jar`) → DevTools detects there are no exploded directory classpath entries to reload and **disables itself**. This dovetails with the restart-classloader design: the restart classloader only loads *directory* classpath entries; a packaged jar has none, so there is nothing for restart to manage. ### Manual force-off - `spring.devtools.restart.enabled=false` disables restart. To ensure the file-watcher thread is never even created, set it as a **System property** before the app starts (e.g. `SpringApplication.setDefaultProperties` or `-D`) rather than in `application.properties`, because by the time properties are read the watcher may already be initializing. - Global disable and other tuning can go in `~/.config/spring-boot/spring-boot-devtools.properties`. ## Why this matters DevTools' behaviors are actively **harmful in production**: disabled template/resource caching kills performance, the file watcher wastes CPU, LiveReload opens a port, and **remote DevTools** (if ever enabled) can execute arbitrary code on the server. Hence the belt-and-suspenders: keep it out of the artifact *and* keep it dormant if it sneaks in. ## Gotchas - Using the wrong scope (e.g. plain `implementation` in Gradle, or forgetting `<optional>`) **will** package DevTools into the fat jar. It'll still self-disable when run from that jar, but it's now shipping unnecessary code. - Some setups run the app as **exploded** (e.g. certain container layered images or `java -cp` against directories). There DevTools could stay active — so relying only on self-disable is risky; declare the scope correctly. - Test classpath: DevTools is not usually wanted in tests; `developmentOnly`/`optional` keeps it out of the test runtime appropriately.
- If DevTools accidentally ends up in your fat jar, does it run in production?No — when the app is launched from a fully packaged jar, DevTools detects there are no exploded directory classpath entries and disables itself. But you're still shipping dead weight, so fix the dependency scope.
- Why set spring.devtools.restart.enabled=false as a System property rather than in application.properties?The restart file-watcher can initialize very early, before application.properties is fully processed. A System property (or SpringApplication default) is read early enough to prevent the watcher thread from ever starting.
saying these in an interview costs you the question
- Declaring DevTools with implementation/compile scope so it gets packaged.
- Believing DevTools stays active in a normal java -jar production run.
- Assuming self-disable alone is enough and scope doesn't matter — exploded runs can keep it active.