Skip to content
ExponaBLOG
JAVA, SPRING FRAMEWORK & SPRING BOOT · DAY 035 / 100

Faster feedback with Spring Boot DevTools for production

Restart in a second instead of rebuilding.

Expona Admin6 min read

Faster feedback with Spring Boot DevTools for production

Waiting for a full Maven rebuild and server restart after every code change kills productivity on large Spring Boot projects. We've all experienced that frustrating cycle: modify a controller, trigger the build, watch the progress bar crawl, then finally test the change. Spring Boot DevTools eliminates this friction by restarting your application in seconds instead of minutes, keeping you in flow while developing.

Automatic restart on classpath changes

Spring Boot DevTools monitors your classpath and automatically restarts the application whenever you save a file. This works through a clever two-classloader system: the base classloader loads framework and dependency classes (which rarely change), while the restart classloader handles your application code. When you modify a file, DevTools only reloads the restart classloader, bypassing the expensive initialization of Spring beans that haven't changed.

To enable DevTools, add the dependency to your pom.xml:

java
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

The optional flag ensures DevTools isn't transitively included in projects that depend on yours, and the runtime scope keeps it out of compiled artifacts. Start your application with mvn spring-boot:run and any change to your Java source files in src/main triggers a restart within 1-2 seconds.

For Gradle projects, add this to build.gradle:

java
dependencies {
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
}

The restart is smart enough to ignore certain patterns. By default, changes to files in .git, .gitignore, build, or dist directories don't trigger restarts. You can customize this by creating a META-INF/spring-devtools.properties file in your classpath:

java
restart.include.mylibs=/mylibs-[\\w-\\.]+\\.jar
restart.exclude.assets=/assets/.*,/templates/.*

In a real e-commerce scenario, you'd modify a product service's business logic, save the file, and see the changes reflected in your next API call within seconds. No need to manually stop and restart the application.

LiveReload for templates and static files

DevTools includes an embedded LiveReload server that automatically refreshes your browser when you change templates, CSS, or JavaScript. This pairs perfectly with frontend development workflows, letting you see styling and layout changes immediately without manual browser refresh.

Install a LiveReload browser extension (available for Chrome, Firefox, and Safari), then enable it for your localhost tab. When you modify a Thymeleaf template in src/main/resources/templates, the browser refreshes automatically:

java
<!-- src/main/resources/templates/products/list.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Products</title>
</head>
<body>
    <h1>Our Products</h1>
    <div th:each="product : ${products}">
        <p th:text="${product.name}">Product Name</p>
        <span th:text="'$' + ${product.price}">Price</span>
    </div>
</body>
</html>

Change the heading text, save, and your browser refreshes within milliseconds. Similarly, edits to CSS files in src/main/resources/static/css are reflected live. This dramatically accelerates frontend work when your backend is serving templates.

You can disable LiveReload in your application.yml if you prefer:

java
spring:
  devtools:
    livereload:
      enabled: false

DevTools respects file patterns in spring.devtools.restart.exclude, so changes to properties files (by default excluded) won't trigger a restart, but LiveReload still works. This is useful for configuration tweaks that don't require a restart.

Disabled automatically in packaged jars

A critical safety feature: DevTools automatically disables itself when your application is packaged as a JAR or deployed to production. This happens because the java -jar launcher detects that the application isn't running from an IDE or mvn spring-boot:run, and the restart mechanism requires a special classloader setup that isn't available in production.

This means you can safely include DevTools in your project dependency list without worrying about it affecting production behavior. The optional flag in the POM reinforces this: it tells Maven not to transitively include DevTools in downstream projects or Docker images.

You can verify DevTools is disabled by checking your application logs when it starts in production:

java
2024-01-15 10:30:45.123 INFO ... Application started in 2.341s (JVM running for 2.856s)

In development with DevTools active, you'd see:

java
2024-01-15 10:30:12.456 INFO ... Application started in 1.234s (process running for 1.567s)

The second output includes process timing. When you're running locally, DevTools silently activates. When packaged as a JAR, it doesn't. This automatic behavior eliminates deployment risk.

Use Testcontainers at dev time for real dependencies

DevTools restarts are only valuable when you're testing against realistic conditions. Many teams use in-memory databases (H2, SQLite) during development to avoid startup overhead, then discover bugs in production when hitting real PostgreSQL or MySQL. Testcontainers solves this by spinning up containerized dependencies on demand.

Add Testcontainers to your dev environment:

java
<dependency>
    <groupId>org.testcontainers<groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.19.7</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers<groupId>
    <artifactId>postgresql</artifactId>
    <version>1.19.7</version>
    <scope>runtime</scope>
</dependency>

Configure Spring to start containers on application launch. Create a configuration class that initializes containers before the application context loads:

java
@Configuration
public class TestcontainersConfiguration {

    @Bean
    public static BeanFactoryPostProcessor containerPostProcessor() {
        return registry -> {
            PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
                .withDatabaseName("testdb")
                .withUsername("testuser")
                .withPassword("testpass");
            postgres.start();
            
            System.setProperty("spring.datasource.url", postgres.getJdbcUrl());
            System.setProperty("spring.datasource.username", postgres.getUsername());
            System.setProperty("spring.datasource.password", postgres.getPassword());
        };
    }
}

With this in place, your first application startup (with or without DevTools) launches a real PostgreSQL container that persists across restarts. Subsequent DevTools restarts reuse the same container, keeping your restart time under 2 seconds while testing against production-grade infrastructure.

This approach catches connection pooling issues, SQL dialect differences, and migration problems early. A team building a travel booking system discovered that N+1 queries that performed acceptably against H2 became problematic with network latency in PostgreSQL—something DevTools + Testcontainers surfaced during development instead of in production.

You can further optimize by using Docker compose in your dev environment, keeping all services (database, Redis, message broker) running between sessions:

java
@Configuration
@EnableConfigurationProperties(DevtoolsProperties.class)
public class DevEnvironmentConfiguration {
    
    // Use Docker Compose services for development
    // Containers survive application restarts
    
}

Common mistakes

  • Forgetting the optional flag: If you omit <optional>true</optional> in your POM, DevTools gets included transitively in projects that depend on yours, bloating their classpath and causing unexpected behavior in their IDEs.
  • Excluding too much from restart: Setting overly broad exclusion patterns (like excluding your entire application package) defeats the purpose. DevTools works best when only framework code is excluded, not your business logic.
  • Relying on in-memory databases in development: Testing against H2 while your production uses PostgreSQL hides bugs. Testcontainers adds negligible overhead and surfaces real issues early. Don't skip this.
  • Not disabling LiveReload for API development: If you're building REST APIs without templates, LiveReload adds unnecessary overhead. Disable it in your development profile to marginally improve restart times.
  • Assuming DevTools works in production: It doesn't, by design. But some teams get surprised when they package their application. Test your packaged JAR locally before deploying to ensure everything works without DevTools.

Key takeaways

  • Spring Boot DevTools cuts restart time from minutes to seconds by using a smart two-classloader architecture that preserves framework state across restarts.
  • LiveReload automatically refreshes your browser when templates or static assets change, eliminating manual F5 workflows.
  • DevTools is automatically disabled in packaged JARs because the restart mechanism requires a development environment; the optional flag prevents transitive inclusion.
  • Pair DevTools with Testcontainers to develop against production-grade infrastructure (PostgreSQL, Redis) without sacrificing fast feedback loops.
  • A few seconds saved on each restart accumulates to hours saved per day across your team—faster feedback is faster learning and faster shipping.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles