Introduction
Slow Spring Boot startup times compound across your entire platform—every deploy waits, every test suite crawls, and every autoscaling event delays traffic handling by minutes instead of seconds. We've learned the hard way in production that startup performance isn't a nice-to-have optimization; it's a reliability multiplier. Speeding up Spring Boot startup teaches us where our configuration is wasteful, which beans we're loading unnecessarily, and how to leverage modern JVM features we often overlook. On Day 88 of our 100-day journey, we'll walk through practical techniques to cut boot time in half or more—using measurement, strategic laziness, pruning, and advanced JVM tooling.
Measure — Use the startup actuator endpoint to find slow beans
You can't optimize what you don't measure. Spring Boot's startup actuator endpoint gives you exact timing data on every bean creation, property source load, and initialization step. Enable it first by adding the actuator dependency and configuring the endpoint.
// pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>Then expose the startup endpoint in your application.properties:
management.endpoints.web.exposure.include=startup
management.endpoint.startup.enabled=trueOnce your app boots, hit the endpoint and capture the full startup timeline:
curl http://localhost:8080/actuator/startup | jq '.' | head -100The response shows you a timeline of every phase: Spring context initialization, bean instantiation, property loading, and more. Look for entries with large duration values—those are your culprits. In a recent production system, we found that a single bean was taking 4.2 seconds to initialize because it was eagerly connecting to a remote config server during startup. By identifying that bean through the startup endpoint, we could make an informed decision about when to connect.
The startup actuator also breaks down by phase:
- Spring context initialization: How long Spring takes to bootstrap its core.
- Bean creation: Individual bean instantiation and dependency injection.
- Property sources: Configuration loading from files, environment variables, and profiles.
Export this data before and after changes so you can quantify your improvements. We typically aim for under 2 seconds on a clean Linux container with standard libraries.
Lazy initialization — spring.main.lazy-initialization helps dev, but hides errors in prod
The temptation to set spring.main.lazy-initialization=true globally is strong. It defers bean creation until first use, which slashes startup time dramatically—sometimes by 50% or more. But we've learned this comes with a serious catch: errors that would surface immediately at startup now hide until the bean is accessed, possibly hours or days later.
// application.properties
spring.main.lazy-initialization=trueThis setting works well in local development, where you'll hit all your endpoints during manual testing. It's less safe in production, where a misconfigured bean might remain undiscovered until a customer triggers the code path.
A better approach is selective lazy initialization. Mark only the beans you're confident about:
@Configuration
public class ReportingConfig {
@Bean
@Lazy
public ReportGenerator heavyReportGenerator() {
// Only instantiated when first injected
return new ReportGenerator();
}
@Bean
public UserService userService() {
// Eager: we want errors at startup
return new UserService();
}
}Use @Lazy on beans that:
- Are expensive to initialize but infrequently used.
- Have optional dependencies that might fail gracefully.
- Connect to external systems on-demand rather than at boot.
For critical beans—database connections, cache clients, authentication providers—always initialize eagerly so that configuration errors fail fast. The extra two seconds at startup is worth the confidence that your entire application stack is healthy.
Trim auto-configuration — Remove unused starters
Every Spring Boot starter brings a cascade of auto-configurations. spring-boot-starter-web includes not just Spring Web but Tomcat, Jackson, Validation, and more. If you're building a gRPC service or a batch job, you're paying startup cost for beans you'll never use.
Start by seeing what auto-configurations actually load:
// Run with debug output
mvn spring-boot:run -Dspring-boot.run.arguments="--debug" 2>&1 | grep "ConditionEvaluationReportListener"Or add this to your main class temporarily:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(Application.class);
app.setLogStartupInfo(true);
app.run(args);
}
}Then check the console output for "AUTO-CONFIGURATION REPORT" to see what Spring enabled.
Audit your pom.xml or build.gradle. Do you really need spring-boot-starter-jpa if you're using plain JDBC? Do you need spring-boot-starter-security for a backend service that only uses API keys in headers?
// Before: pulling in security, ORM, and transaction management
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
// After: only what you need
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>We've shaved 800ms off a microservice's startup by removing transitive dependencies and being explicit about what we actually import. In one case, a service included spring-boot-starter-mail for a feature that had been deprecated two years prior.
Use mvn dependency:tree to visualize the full tree and spot bloat:
mvn dependency:tree | grep -E "^\[INFO\] [+|\]"CDS and AOT — Class Data Sharing archives cut JVM startup significantly
The JVM does a lot of work at startup: loading classes, verifying bytecode, building internal data structures. Class Data Sharing (CDS) archives precompute much of this work and serialize it to disk, so subsequent runs skip those expensive steps.
For Java 17 and later, create a CDS archive with a single command:
java -XX:+UseAppCDS -XX:DumpLoadedClassList=classes.lst -jar app.jarThen generate the archive:
java -XX:+UseAppCDS -XX:SharedClassListFile=classes.lst -XX:SharedArchiveFile=app-cds.jsa -Xshare:dump -jar app.jarNow run with the archive:
java -XX:+UseAppCDS -XX:SharedArchiveFile=app-cds.jsa -jar app.jarWe've seen startup improvements of 15–25% using CDS alone, especially on Java 19+. The archive is small (usually under 100MB) and can be baked into your Docker image.
For even more dramatic gains, Spring Boot 3.0+ supports Ahead-of-Time (AOT) compilation via GraalVM native image. This compiles your entire Spring application to native machine code, eliminating the JVM startup altogether:
// In your pom.xml
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
// Build native image
mvn -Pnative native:compileNative images start in 50–200ms instead of 2–5 seconds, and use dramatically less memory. The trade-offs are that build times increase, reflection and dynamic class loading become harder, and you're locked into the platform you build on.
For most teams, CDS is a better starting point: it's low-risk, easy to implement, and gives solid returns. AOT makes sense for serverless functions or autoscaling fleets where cold start time is business-critical.
Common mistakes
Not measuring before optimizing: We've spent hours tuning a bean that represented 2% of startup time, while ignoring a data loader taking 30%. Always use the startup actuator first.
Going all-in on lazy initialization: Deferring everything feels fast but delays error discovery. Reserve @Lazy for truly optional or heavyweight beans.
Removing starters without testing: Auto-configurations often wire up multiple features. Removing a starter can break features you didn't realize you were using. Always test your application endpoints after pruning dependencies.
Building CDS archives on one OS and running on another: CDS archives are platform and JVM version specific. Build and test on the exact OS and Java version you'll use in production.
Ignoring Spring's profiling tools: The startup endpoint, property debug output, and bean graph visualization exist for a reason. Use them before resorting to custom timing code.
Key takeaways
- Measure first: the startup actuator endpoint shows you exactly where boot time disappears.
- Use selective lazy initialization on heavyweight or optional beans; keep critical infrastructure eager to fail fast.
- Audit your dependencies ruthlessly—every unused starter is wasted JVM startup work.
- Class Data Sharing (CDS) is a low-risk win on Java 17+, typically cutting startup by 15–25%.
- Reserve AOT and native images for scenarios where every millisecond counts (serverless, aggressive autoscaling).
- Test thoroughly after changes; startup optimization can mask configuration errors if not done carefully.
- Fast startup improves deploy velocity, test feedback loops, and autoscaling responsiveness—all core production concerns.






Comments
Loading comments…