Introduction
Improving Spring Boot’s startup time could seem like something that developers should be proud of. You can stretch into uncomfortable territory, though, if the application counts hundreds of beans. For instance, you have the option to use "spring.main.lazy-initialization=true“, a property that defers bean creation until the moment they’re actually needed (Lazy-Initialization).
It sounds like a nice and easy way to reduce your application startup time. In reality, that fundamentally alters your application’s lifecycle semantics. If you’re not paying close attention, it will silently skip critical initialization logic until the worst possible moment.
What Lazy Initialization Actually Changes
Under normal initialization (eager), Spring Boot creates every singleton bean during the application context refresh phase. This means your beans are fully constructed, dependencies injected, and lifecycle callbacks executed before your application starts accepting traffic. If something is misconfigured, the application fails fast during startup, and this prevents any final user from seeing any error.
When you enable lazy initialization, Spring changes the above approach. Bean definitions are still registered in the context, but no instance is created until a request first asks for that bean. The bean reference exists, but the object itself remains unmaterialized.
This deferral applies to the entire bean lifecycle, including the @PostConstruct phase. Methods annotated with @PostConstruct don’t run at startup anymore. They run at first use, whenever that happens to be.
The @PostConstruct Deferral Problem
The @PostConstruct‘s annotation is widely used for initialization that must happen after dependency injection but before the bean is considered ready. Common patterns include:
- Validating external service connectivity
- Warming caches to prevent cold-start latency
- Registering event listeners or metrics
- Loading reference data into memory
- Establishing database connection pools
With lazy initialization enabled, none of this happens at startup. It happens later, possibly much later. And that delay introduces several categories of risk.
Delayed Failure Discovery
Consider a bean that validates its configuration during initialization:
@Component
public class PaymentGatewayClient {
@PostConstruct
public void validateConnection() {
this.apiClient.ping();
// Throws if credentials are wrong or service is unreachable
}
}
Under eager initialization, a bad API key or network issue causes the application to crash immediately on startup. Your deployment pipeline catches it. Monitoring alerts fire. The problem is contained.
With lazy initialization, the application starts successfully. Health checks return green. The bad configuration sits dormant until the first payment request arrives, potentially hours after deployment. Then the bean is created, @PostConstruct fires, and the exception propagates to a real user transaction.
Broken Timing Assumptions
Many applications rely on initialization happening in a predictable sequence. A cache warmer might populate an in-memory store so that the first user request is fast:
@Component
public class ProductCache {
@PostConstruct
public void warmCache() {
List<Product> products = productRepository.findAll();
products.forEach(this::cache);
}
}
With lazy initialization, the cache remains empty at startup. The first request that touches ProductCache triggers both the cache warming and the original request processing. That first user pays the full cost of the database query, experiencing latency that your load tests never captured because they happened to trigger cache creation before measuring response times.
Missed Early Events
Beans that register themselves during initialization may miss application events that fire shortly after startup:
@Component
public class AuditLogInitializer {
@PostConstruct
public void registerHandler() {
eventBus.register(this);
}
@EventListener
public void onConfigRefresh(ConfigRefreshedEvent event) {
// This might never fire if the bean wasn't created yet
}
}
If ConfigRefreshedEvent is published during the startup sequence, but after context refresh, a lazily initialized bean won’t be registered to receive it.
When Lazy Initialization Is Actually Appropriate
Lazy initialization is still genuinely useful in specific scenarios:
Development environments, where you want the fastest possible feedback loop and don’t care if some beans are never instantiated during a particular test run.
Optional features that are only accessed conditionally. If a bean is only used by an admin endpoint that receives traffic once a week, deferring its creation saves memory and startup time.
CLI tools and batch jobs where startup speed matters more than runtime latency, and where the execution path is deterministic enough that you know exactly which beans will be touched.
A Safer Approach: Selective Laziness
Rather than enabling lazy initialization globally, consider applying it selectively:
@Lazy
@Component
public class ExpensiveReportGenerator {
// Only created when a report is actually requested
}
This gives you startup benefits for genuinely heavy, rarely-used beans without altering the lifecycle semantics of your core application infrastructure.
If you do enable the global flag, add explicit startup validation for critical paths:
@Component
public class StartupValidator implements ApplicationRunner {
@Autowired
private PaymentGatewayClient paymentClient;
@Autowired
private ProductCache productCache;
@Override
public void run(ApplicationArguments args) {
// Force initialization of critical beans at startup
// This ensures @PostConstruct runs before traffic arrives
}
}
Conclusion
spring.main.lazy-initialization=true is not a performance optimization you can toggle on without understanding the trade-offs. It fundamentally changes when your application validates itself, when it prepares its resources, and when it discovers configuration errors.
The property doesn’t just delay bean creation. It delays your application’s entire self-validation sequence. In production systems, that delay translates directly into deferred failures, problems that surface not during deployment, but during real user transactions.
Use it with intention, apply it selectively, and never assume that a faster startup time comes without hidden costs.
