Every morning, our team lost close to a minute doing something that should take seconds: waiting for the Spring Boot application to boot up on local. Multiply that across restarts during debugging, CI pipeline runs, and container cold starts in production, and it added up to hours of dead time every week. Forty-five seconds doesn’t sound catastrophic until you’re the one staring at a spinning terminal for the twentieth time before lunch.
This is the story of how I took that number down to 8 seconds - an 82% reduction - without touching business logic, without ripping out Spring, and without heroics. Just a systematic hunt through what actually happens between main() and “Started Application in X seconds.”
Why Startup Time Actually Matters
It’s tempting to dismiss startup time as a “nice to have.” It isn’t, for three concrete reasons:
-
Developer feedback loop - every restart during local development eats into flow state. Slow startup compounds across a team into real lost hours.
-
CI/CD pipeline cost - integration tests that spin up a Spring context pay the startup tax on every single run, every single commit.
-
Autoscaling and cold starts - in containerized and serverless environments, a slow-booting app means slower scale-out under load and worse cost efficiency.
With that framed, here’s exactly where the 45 seconds were going, and what I did about each one.
Step 1: Actually Measure Before Optimizing
The first mistake most people make is guessing. Before changing a single line, I turned on Spring Boot’s built-in startup diagnostics to see where time was really being spent.
# application.yml
spring:
jmx:
enabled:false
management:
endpoints:
web:
exposure:
include: health,info
More usefully, I enabled ApplicationStartup tracking to get a full breakdown of every startup phase:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(Application.class);
app.setApplicationStartup(new BufferingApplicationStartup(2048));
app.run(args);
}
}This single change surfaced the real breakdown of our 45 seconds:
|
Phase |
Time Spent |
% of Total |
|---|---|---|
|
Component scanning & bean definition |
14.2s |
31% |
|
Bean instantiation & autowiring |
11.5s |
26% |
|
Database connection pool init |
8.1s |
18% |
|
Auto-configuration evaluation |
6.4s |
14% |
|
Third-party library init (Kafka, Redis clients) |
3.3s |
7% |
|
Everything else |
1.5s |
4% |
You can’t optimize what you haven’t measured. Component scanning and bean instantiation alone were more than half our startup time - and neither is “business logic.” That’s pure framework overhead waiting to be trimmed.
Step 2: Narrow Component Scanning
By default, @SpringBootApplication scans the entire package tree from the application root. In a mid-sized codebase with 400+ classes, that’s a lot of unnecessary classpath scanning for beans that live in three or four actual packages.
// Before: implicit, unbounded scanning
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// After: explicit, scoped scanning
@SpringBootApplication(scanBasePackages = {
"com.company.app.service",
"com.company.app.controller",
"com.company.app.config",
"com.company.app.repository"
})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}This alone shaved roughly 4 seconds off. Small win, but free - it’s a one-line change with zero behavioral risk.
Step 3: Turn Off Auto-Configuration You Don’t Use
Spring Boot’s auto-configuration is a double-edged sword. It’s what makes the framework feel magical, but every @ConditionalOnClass check it evaluates costs time - even for features you never touch. I used the auto-configuration report to find dead weight.
# See exactly which auto-configurations were applied vs skipped
java -jar app.jar --debugThat report showed we were pulling in auto-configuration for JMX, Groovy templating, and a full JPA repository scan - none of which we actually used in most services. I excluded them explicitly:
@SpringBootApplication(exclude = {
JmxAutoConfiguration.class,
GroovyTemplateAutoConfiguration.class
})
public class Application {
// ...
}And in application.properties, I disabled lazy features we weren’t using at all:
spring.jmx.enabled=false
spring.main.banner-mode=off
spring.jpa.open-in-view=falsespring.jpa.open-in-view=false is worth calling out on its own - beyond the startup benefit, it prevents a well-known production footgun where the persistence context stays open through the entire HTTP request, masking N+1 query problems and holding database connections longer than necessary.
Step 4: Switch On Lazy Initialization
This was the single biggest win. Spring Boot 2.2+ ships with a global lazy initialization flag that defers bean creation until the bean is actually needed, rather than eagerly wiring the entire application graph at startup.
spring.main.lazy-initialization=trueOne line. Nine seconds saved. The tradeoff is real, though, and worth understanding rather than blindly flipping the switch:
-
The first request to any given endpoint will be slightly slower, since that’s when its beans actually get instantiated.
-
Beans with
@PostConstructside effects (health checks, cache warmers, connection validators) may need to be explicitly excluded from laziness. -
It can mask configuration errors that would otherwise fail fast at startup - a bad bean definition might not surface until the endpoint is hit.
For beans where eager loading actually matters - like a connection pool warm-up - I opted them back in individually:
@Component
@Lazy(false)
public class ConnectionPoolWarmer {
// eagerly initialized despite the global lazy flag
}Step 5: Fix the Database Connection Pool
8.1 seconds - nearly a fifth of our total startup time - was going into HikariCP establishing its full pool of connections before the app was marked ready. The default pool size and validation settings were tuned for production load, not startup speed.
# Before
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.maximum-pool-size=20
# After — smaller initial pool, scales up under real load
spring.datasource.hikari.minimum-idle=2
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.initialization-fail-timeout=1
spring.datasource.hikari.connection-timeout=5000Dropping minimum-idle meant Hikari no longer opened ten connections synchronously before the context was considered ready - it opens two, and grows the pool lazily as real traffic arrives. That single config block cut roughly 5 seconds.
Step 6: Move to Class Data Sharing (CDS)
The last big lever wasn’t a Spring change at all - it was a JVM one. Application Class Data Sharing lets the JVM pre-parse and cache class metadata so it doesn’t have to re-verify and re-load classes from scratch on every startup.
# Step 1: Generate a class list from a normal run
java -Xshare:off -XX:DumpLoadedClassList=app.classlist \
-jar app.jar --spring.main.web-application-type=none
# Step 2: Create the shared archive
java -Xshare:dump -XX:SharedClassListFile=app.classlist \
-XX:SharedArchiveFile=app.jsa -cp app.jar
# Step 3: Run using the archive
java -Xshare:on -XX:SharedArchiveFile=app.jsa -jar app.jarOn Java 17+, Spring Boot also has direct support for this through the spring-boot-maven-plugin and -Dspring.aot.enabled=true for full ahead-of-time processing - a longer-term investment if you’re on a recent Boot version, but CDS alone got us most of the way.
The Results
Here’s the full before-and-after, broken down by the same phases I measured at the start:
|
Optimization |
Time Saved |
|
|---|---|---|
|
Scoped component scanning |
~4.0s |
|
|
Removed unused auto-configuration |
~3.5s |
|
|
Lazy initialization |
~9.0s |
|
|
HikariCP pool tuning |
~5.0s |
|
|
Class Data Sharing (CDS) |
~15.5s |
|
|
Total reduction |
~37s (45s → 8s) |
None of these changes touched a single line of business logic. This is the part that surprised me most - the biggest wins were almost entirely configuration and JVM-level, not code-level.
Key Takeaways
-
Measure first. ApplicationStartup tracking and the --debug auto-configuration report will tell you exactly where your time goes - don’t guess.
-
Lazy initialization and CDS are the highest-leverage, lowest-effort wins available on almost any Spring Boot app.
-
Your connection pool settings are probably tuned for steady-state load, not cold start - they deserve a second look.
-
Every optimization here is reversible and low-risk. None of it required rewriting application logic.
If you’re running a Spring Boot service that takes more than 15-20 seconds to start, there’s a very good chance at least three of these six changes apply to you directly. Start with ApplicationStartup tracking - you can’t fix what you haven’t measured.
What’s your team’s current Spring Boot startup time? I’d be curious to hear what’s working for others tackling the same problem.

Discussion