Spring Boot DevTools: Automatic Restart and Productivity

Introduction

This tutorial covers Spring Boot DevTools, a dependency that makes the edit-compile-run cycle of development faster. We will start with what DevTools does and how to add it to a project. We will then watch its main feature, automatic restart, in a real session: start an application, change a controller while it runs, recompile, and see the new code answer without restarting by hand. Next we will see how DevTools makes restarts fast with two class loaders, which files trigger a restart and how to control that, and the development property defaults it switches on. Lastly, we will confirm that DevTools switches itself off in a packaged application, look at the state of LiveReload in Spring Boot 4, set up IntelliJ IDEA and Eclipse, and list a few other productivity helpers.

(This tutorial uses the application from Your First Spring Boot Application with Spring InitializrClick to read tutorial: Your First Spring Boot Application with Spring Initializr.)

What DevTools does

Without DevTools, every code change during development means stopping the application, rebuilding and starting it again - and for a real application, startup can take many seconds. spring-boot-devtools adds -
  • Automatic restart: when compiled classes change, the application restarts itself, quickly, keeping the JVM running.
  • Development property defaults: settings that suit development, such as switching off template caches so that edited templates show up at once.
  • Global settings: a personal configuration file in your home directory that applies to every Spring Boot project you run with DevTools.
  • Remote support: restarting an application running elsewhere (rarely used, and off by default).
DevTools is strictly a development aid: it is not packaged into the final application and switches itself off in production, as we will verify.

Adding DevTools

On start.spring.io, add the Spring Boot DevTools dependency. Spring Initializr writes it into pom.xml like this -
pom.xml - the DevTools dependency, as Spring Initializr adds it
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-devtools</artifactId>
  <scope>runtime</scope>
  <optional>true</optional>
</dependency>
runtime scope keeps DevTools out of the compile classpath, so no application code can depend on it, and optional stops it from leaking into other projects that depend on this one. In Gradle, the equivalent is the developmentOnly configuration: developmentOnly 'org.springframework.boot:spring-boot-devtools'.

The example project, first-app-devtools, is the first application plus this dependency and one extra bean that logs a line each time the application becomes ready, including which class loader loaded it -
ReadyLogger.java - logs a line each time the application is ready
package com.javabrahman.firstapp;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class ReadyLogger {

	private static final Logger log = LoggerFactory.getLogger(ReadyLogger.class);

	@EventListener(ApplicationReadyEvent.class)
	public void ready() {
		log.info("Application ready - loaded by {}", getClass().getClassLoader().getClass().getSimpleName());
	}

}

Automatic restart in action

The session below is real and fully automated. It starts the application with ./mvnw spring-boot:run, calls /hello, then - while the application keeps running - replaces the greeting in HelloController and recompiles with ./mvnw compile, as you would by saving the file and building in an IDE, and calls /hello again. The changed line of the controller is -
HelloController.java - the line changed while the application runs
// before
return "Hello, " + name + "!";
// after
return "Hi there, " + name + "! (changed while the application was running)";
 OUTPUT of the two requests, before and after recompiling
> curl http://localhost:8080/hello
Hello, World!

> curl http://localhost:8080/hello
Hi there, World! (changed while the application was running)
The second request was answered by the new code, although nobody restarted the application. Here is what the application logged during the session -  OUTPUT - the application's console log
2026-11-06T13:42:01.939+05:30  INFO 8508 --- [first-app-devtools] [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2026-11-06T13:42:01.948+05:30  INFO 8508 --- [first-app-devtools] [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2026-11-06T13:42:05.298+05:30  INFO 8508 --- [first-app-devtools] [  restartedMain] o.s.boot.tomcat.TomcatWebServer          : Tomcat initialized with port 8080 (http)
2026-11-06T13:42:05.363+05:30  INFO 8508 --- [first-app-devtools] [  restartedMain] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2026-11-06T13:42:05.371+05:30  INFO 8508 --- [first-app-devtools] [  restartedMain] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/11.0.27]
2026-11-06T13:42:05.539+05:30  INFO 8508 --- [first-app-devtools] [  restartedMain] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in 3591 ms
2026-11-06T13:42:07.186+05:30  INFO 8508 --- [first-app-devtools] [  restartedMain] o.s.boot.tomcat.TomcatWebServer          : Tomcat started on port 8080 (http) with context path '/'
2026-11-06T13:42:07.267+05:30  INFO 8508 --- [first-app-devtools] [  restartedMain] com.javabrahman.firstapp.ReadyLogger     : Application ready - loaded by RestartClassLoader
2026-11-06T13:42:07.461+05:30  INFO 8508 --- [first-app-devtools] [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2026-11-06T13:42:07.461+05:30  INFO 8508 --- [first-app-devtools] [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2026-11-06T13:42:07.469+05:30  INFO 8508 --- [first-app-devtools] [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Completed initialization in 8 ms
2026-11-06T13:42:20.660+05:30  INFO 8508 --- [first-app-devtools] [   File Watcher] rtingClassPathChangeChangedEventListener : Restarting due to 4 class path changes (0 additions, 4 deletions, 0 modifications)
2026-11-06T13:42:20.766+05:30  INFO 8508 --- [first-app-devtools] [       Thread-1] o.s.boot.tomcat.GracefulShutdown         : Commencing graceful shutdown. Waiting for active requests to complete
2026-11-06T13:42:20.798+05:30  INFO 8508 --- [first-app-devtools] [tomcat-shutdown] o.s.boot.tomcat.GracefulShutdown         : Graceful shutdown complete
2026-11-06T13:42:22.318+05:30  INFO 8508 --- [first-app-devtools] [  restartedMain] o.s.boot.tomcat.TomcatWebServer          : Tomcat initialized with port 8080 (http)
2026-11-06T13:42:22.326+05:30  INFO 8508 --- [first-app-devtools] [  restartedMain] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2026-11-06T13:42:22.327+05:30  INFO 8508 --- [first-app-devtools] [  restartedMain] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/11.0.27]
2026-11-06T13:42:22.392+05:30  INFO 8508 --- [first-app-devtools] [  restartedMain] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in 911 ms
2026-11-06T13:42:22.757+05:30  INFO 8508 --- [first-app-devtools] [  restartedMain] o.s.boot.tomcat.TomcatWebServer          : Tomcat started on port 8080 (http) with context path '/'
2026-11-06T13:42:22.778+05:30  INFO 8508 --- [first-app-devtools] [  restartedMain] .ConditionEvaluationDeltaLoggingListener : Condition evaluation unchanged
2026-11-06T13:42:24.191+05:30  INFO 8508 --- [first-app-devtools] [   File Watcher] rtingClassPathChangeChangedEventListener : Restarting due to 4 class path changes (4 additions, 0 deletions, 0 modifications)
2026-11-06T13:42:24.197+05:30  INFO 8508 --- [first-app-devtools] [       Thread-5] o.s.boot.tomcat.GracefulShutdown         : Commencing graceful shutdown. Waiting for active requests to complete
2026-11-06T13:42:24.201+05:30  INFO 8508 --- [first-app-devtools] [tomcat-shutdown] o.s.boot.tomcat.GracefulShutdown         : Graceful shutdown complete
2026-11-06T13:42:25.740+05:30  INFO 8508 --- [first-app-devtools] [  restartedMain] o.s.boot.tomcat.TomcatWebServer          : Tomcat initialized with port 8080 (http)
2026-11-06T13:42:25.749+05:30  INFO 8508 --- [first-app-devtools] [  restartedMain] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2026-11-06T13:42:25.749+05:30  INFO 8508 --- [first-app-devtools] [  restartedMain] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/11.0.27]
2026-11-06T13:42:25.805+05:30  INFO 8508 --- [first-app-devtools] [  restartedMain] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in 961 ms
2026-11-06T13:42:26.075+05:30  INFO 8508 --- [first-app-devtools] [  restartedMain] o.s.boot.tomcat.TomcatWebServer          : Tomcat started on port 8080 (http) with context path '/'
2026-11-06T13:42:26.093+05:30  INFO 8508 --- [first-app-devtools] [  restartedMain] .ConditionEvaluationDeltaLoggingListener : Condition evaluation unchanged
2026-11-06T13:42:26.095+05:30  INFO 8508 --- [first-app-devtools] [  restartedMain] com.javabrahman.firstapp.ReadyLogger     : Application ready - loaded by RestartClassLoader
2026-11-06T13:42:26.123+05:30  INFO 8508 --- [first-app-devtools] [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2026-11-06T13:42:26.126+05:30  INFO 8508 --- [first-app-devtools] [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2026-11-06T13:42:26.130+05:30  INFO 8508 --- [first-app-devtools] [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Completed initialization in 3 ms

Explanation of the log

  • The first two lines come from DevTools: its development property defaults are active, with a hint about web logging.
  • The application runs in a thread called restartedMain, not main, and ReadyLogger was loaded by a RestartClassLoader - DevTools' class loader, explained in the next section.
  • After the recompile, DevTools' File Watcher thread noticed the changed class files and logged Restarting due to 4 class path changes. Maven's incremental compiler first deletes the old class files and then writes the new ones; when the two steps fall into separate checks, DevTools sees them as two changes - deletions, then additions - and restarts twice in quick succession. Either way, the last restart loads the new classes.
  • The old application context was shut down - Tomcat's graceful shutdown ran - and a new one started in a fresh restartedMain thread. Condition evaluation unchanged means the restart did not change which auto-configurations applied.
  • Compare the initialization completed in ... ms lines: each restart took a fraction of the time of the first start, because the JVM and all the library classes were already loaded.

How restart works: two class loaders

A restart is faster than a cold start because DevTools splits the classpath in two -
  • The base class loader loads the classes that do not change during development - every jar: Spring, Tomcat, Jackson and the rest.
  • The restart class loader loads the classes you are working on - your project's target/classes folder.
On a restart, DevTools closes the application context, throws the restart class loader away, creates a new one, and starts the application again in it. The base class loader, with its thousands of already-loaded library classes, stays. This is a real restart of your code - every bean is created afresh - not a "hot swap" of changed methods, so it works for any kind of change, including new classes, new beans and changed configuration classes.

Occasionally the split causes a problem, typically when a library caches classes it loaded from your code - the symptom is a ClassCastException between two classes with the same name. The META-INF/spring-devtools.properties file can then move a jar into the restart class loader with a restart.include.* entry.

What triggers a restart

DevTools watches the classpath folders of the project and restarts when a file in them changes. Some files are excluded because they can be served without a restart - by default anything under static/, public/, resources/, templates/ and META-INF/resources/, compiled test classes, and build information files. The behaviour is controlled by properties in the spring.devtools.restart group -
  • spring.devtools.restart.enabled=false switches restart off while keeping the other DevTools features.
  • spring.devtools.restart.exclude replaces the default exclusions, and additional-exclude adds to them.
  • spring.devtools.restart.additional-paths watches extra folders, for example a module built separately.
  • spring.devtools.restart.poll-interval (default 1s) and quiet-period (default 400ms) control how often the classpath is checked, and how long it must stay unchanged before restarting - so a build that writes many files causes one restart, not many.
  • spring.devtools.restart.trigger-file restarts only when one named file changes. Some developers point it at a file their IDE touches on demand, to restart exactly when they choose rather than on every compile.

Development property defaults

Several Spring Boot features cache aggressively for production speed, which gets in the way during development. DevTools switches them off - template caching for Thymeleaf, FreeMarker, Groovy and Mustache, HTTP caching of static resources - and turns on some developer-friendly options, such as including error details in error responses. These defaults appear in the first log line of the session above, and spring.devtools.add-properties=false disables them.

Global settings: properties in spring-boot-devtools.properties (or .yml) in the $HOME/.config/spring-boot/ folder apply to every application you run with DevTools - a good place for personal preferences, such as a trigger file, that should not be committed to a project.

DevTools is off in a packaged application

DevTools must never run in production, and Spring Boot makes sure of it in two ways. The Spring Boot Maven and Gradle plugins leave DevTools out of the packaged jar by default. And even if it were on the classpath, DevTools disables itself when the application is started from a jar with java -jar, or from a special class loader, which it treats as a production launch. Building the same project and running its jar -  OUTPUT of a request to the packaged application
> curl http://localhost:8080/hello
Hello, World!
 OUTPUT - the packaged application's console log
2026-11-06T13:42:40.119+05:30  INFO 28368 --- [first-app-devtools] [           main] o.s.boot.tomcat.TomcatWebServer          : Tomcat initialized with port 8080 (http)
2026-11-06T13:42:40.186+05:30  INFO 28368 --- [first-app-devtools] [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2026-11-06T13:42:40.195+05:30  INFO 28368 --- [first-app-devtools] [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/11.0.27]
2026-11-06T13:42:40.311+05:30  INFO 28368 --- [first-app-devtools] [           main] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in 5217 ms
2026-11-06T13:42:42.608+05:30  INFO 28368 --- [first-app-devtools] [           main] o.s.boot.tomcat.TomcatWebServer          : Tomcat started on port 8080 (http) with context path '/'
2026-11-06T13:42:42.704+05:30  INFO 28368 --- [first-app-devtools] [           main] com.javabrahman.firstapp.ReadyLogger     : Application ready - loaded by LaunchedClassLoader
2026-11-06T13:42:43.272+05:30  INFO 28368 --- [first-app-devtools] [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2026-11-06T13:42:43.274+05:30  INFO 28368 --- [first-app-devtools] [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2026-11-06T13:42:43.281+05:30  INFO 28368 --- [first-app-devtools] [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Completed initialization in 7 ms
There are no DevTools lines, the thread is main, and ReadyLogger reports Spring Boot's ordinary LaunchedClassLoader from the executable jar - DevTools is not there. (Setting the system property -Dspring.devtools.restart.enabled=false also forces restart off, which is useful when an IDE launches the application in an unusual way.)

LiveReload in Spring Boot 4

Earlier versions of DevTools also started a LiveReload server, which told a browser extension to refresh the page whenever resources changed. In Spring Boot 4 this feature is deprecated and off by default: the 4.1.2 DevTools metadata lists spring.devtools.livereload.enabled with the default false and marks it deprecated, and the browser extensions it relied on are no longer maintained. Setting the property to true still works for now, but for front-end work a dedicated front-end development server with hot module replacement is the better tool.

Setting up your IDE

Automatic restart reacts to compiled classes, so the IDE has to compile your changes -
  • Eclipse (with the Spring Tools) builds automatically when a file is saved, so saving is enough.
  • IntelliJ IDEA compiles on Build > Build Project (Ctrl+F9). For a restart on every save, enable Build project automatically under Settings > Build, Execution, Deployment > Compiler, and in Settings > Advanced Settings allow auto-make to start even if the application is running.
  • VS Code with the Java and Spring Boot extensions compiles on save by default.
Start the application from the IDE's Spring Boot run configuration or with ./mvnw spring-boot:run; either way, DevTools is on the classpath and active.

Other productivity helpers

  • Docker Compose support (spring-boot-docker-compose): with a compose.yaml in the project, Spring Boot starts the services in it - a database, Redis, a message broker - when the application starts, and configures the connection properties automatically.
  • Testcontainers at development time: a test-scope main class can start the application with the same throw-away containers the integration tests use (covered in module 6).
  • The condition evaluation report with --debug, from the auto-configuration tutorialClick to read tutorial: How Spring Boot Auto-Configuration Works, when a bean is unexpectedly missing.
  • Spring Boot Actuator, covered in module 11, for inspecting beans, configuration properties and mappings of a running application.

Summary

In this tutorial we first saw what DevTools offers and added it with runtime scope and optional, as Spring Initializr does. We then ran a real session in which a controller was changed and recompiled while the application ran, and DevTools restarted it automatically so that the next request got the new answer. We saw why restarts are fast - a base class loader for the libraries and a restart class loader for our classes - which files trigger a restart and the properties that control it, and the development property defaults and global settings. Next we confirmed that DevTools is absent from the packaged jar, and saw that LiveReload is deprecated and off by default in Spring Boot 4. Lastly, we set up the IDEs and listed other productivity helpers. The next tutorial packages the application for production: executable and layered jarsClick to read tutorial: Packaging Spring Boot Apps: Executable and Layered JARs.