Spring Boot
Lazy Initialization
By default, Spring creates all singleton beans at application startup — eager initialization. Lazy initialization defers bean creation until the bean is first requested, reducing startup time at the cost of slower first access. Understanding when to use lazy initialization, its trade-offs, and how to apply it globally or selectively is essential for optimizing Spring Boot application startup.
Eager vs Lazy Initialization
Spring's default behavior is eager initialization — all singleton beans are created, configured, and wired at application startup before the first request is handled. This has clear advantages: configuration errors surface immediately at startup rather than during a production request, and every request after startup has instant access to all beans.
Lazy initialization is the alternative. A lazy bean is not created until the first time it is requested — either through direct injection or through getBean(). The bean is then cached as a singleton and reused for all subsequent requests.
The trade-off is direct: eager initialization means longer startup time but instant first-request performance and early error detection. Lazy initialization means faster startup but slower first-request performance and deferred error detection — a missing bean or misconfiguration is discovered when the bean is first needed, not at startup.
Lazy initialization is most valuable when: the application has many beans that are rarely or never used in typical operation (admin tools, batch jobs, reporting services), startup time is critical (serverless functions, frequent restarts in development), or you are optimizing a large monolith before extracting microservices.
@Lazy on Individual Beans
@Lazy on a class or @Bean method defers creation of that specific bean until it is first accessed. All other beans are still created eagerly at startup.
Java
// @Lazy on a @Component class:
@Service
@Lazy
public class HeavyReportingService {
private final ReportRepository reportRepository;
private final PdfGenerator pdfGenerator;
private final ChartRenderer chartRenderer;
@PostConstruct
public void initialize() {
// This runs only when the bean is first requested — not at startup
System.out.println("HeavyReportingService initialized on first use");
// Load report templates, warm up PDF engine, etc.
}
public HeavyReportingService(ReportRepository reportRepository,
PdfGenerator pdfGenerator,
ChartRenderer chartRenderer) {
this.reportRepository = reportRepository;
this.pdfGenerator = pdfGenerator;
this.chartRenderer = chartRenderer;
}
public byte[] generateAnnualReport(int year) {
// ... expensive report generation
return new byte[0];
}
}
// @Lazy on a @Bean method:
@Configuration
public class InfrastructureConfig {
// Created immediately at startup:
@Bean
public DataSource dataSource() {
return DataSourceBuilder.create().url("jdbc:mysql://...").build();
}
// Created only when first injected or getBean() is called:
@Bean
@Lazy
public ElasticsearchClient elasticsearchClient() {
// Expensive to initialize — skip if search feature not used
return ElasticsearchClients.createDefault(
ElasticsearchTransport.builder()
.serverUrl("https://search.example.com:9200")
.build()
);
}
// Lazy with custom name:
@Bean(name = "analyticsClient")
@Lazy
public AnalyticsClient analyticsClient(
@Value("${analytics.api.key}") String apiKey) {
return new MixpanelAnalyticsClient(apiKey);
}
}@Lazy at the Injection Point
@Lazy can also be placed at the injection point — on a constructor parameter, setter parameter, or field. This injects a lazy-resolution proxy: the bean is created only when a method is first called on the injected object, not when the owning bean is created.
Java
// @Lazy at the injection point — proxy injected instead of real bean:
@Service
public class OrderService {
private final OrderRepository orderRepository; // created eagerly
private final PaymentService paymentService; // created eagerly
// EmailService is lazy — a proxy is injected, real bean created on first use:
private final EmailService emailService;
public OrderService(
OrderRepository orderRepository,
PaymentService paymentService,
@Lazy EmailService emailService) { // lazy proxy injected here
this.orderRepository = orderRepository;
this.paymentService = paymentService;
this.emailService = emailService;
// EmailService bean has NOT been created yet at this point
// A CGLIB proxy for EmailService is assigned to this.emailService
}
public Order createOrder(CreateOrderRequest request) {
PaymentResult payment = paymentService.charge(
request.customerId(), request.totalAmount()
);
Order saved = orderRepository.save(new Order(request, payment));
// EmailService bean is created HERE — on the first method call:
emailService.sendConfirmation(saved); // proxy delegates to real bean
return saved;
}
// If createOrder is never called, EmailService is never created
}
// @Lazy on a setter parameter:
@Service
public class NotificationService {
private EmailService emailService;
@Autowired
public void setEmailService(@Lazy EmailService emailService) {
this.emailService = emailService; // proxy injected
}
}
// How the lazy proxy works:
// 1. Spring creates a CGLIB subclass of EmailService (or JDK proxy for interfaces)
// 2. This proxy is injected into OrderService at construction time
// 3. When emailService.sendConfirmation() is called on the proxy,
// the proxy looks up the real EmailService from the ApplicationContext
// 4. The real EmailService is created (if not already) and the call is delegated
// 5. Subsequent calls use the already-created real bean directlyGlobal Lazy Initialization
Spring Boot 2.2 introduced global lazy initialization — all beans become lazy with a single property. This can dramatically reduce startup time for large applications, with important caveats.
Java
# Enable global lazy initialization — ALL singleton beans become lazy:
# application.properties:
spring.main.lazy-initialization=true
# Or programmatically:
# SpringApplication app = new SpringApplication(Application.class);
# app.setLazyInitialization(true);
# app.run(args);
// Effects of global lazy initialization:
// - All @Service, @Repository, @Component beans: lazy
// - All @Bean methods in @Configuration: lazy
// - Spring Boot auto-configured beans: lazy
// - Startup time reduced — often by 40-60% for large applications
// - First request to any endpoint will be slower (beans created on demand)
// - Configuration errors surface on first use, not at startup
// CRITICAL: Some beans MUST be created eagerly even with global lazy enabled:
// Override lazy for specific beans with @Lazy(false):
@Component
@Lazy(false) // force eager even when spring.main.lazy-initialization=true
public class DatabaseMigrationRunner {
@PostConstruct
public void runMigrations() {
// Must run at startup BEFORE any database operations
flyway.migrate();
}
}
@Component
@Lazy(false)
public class ApplicationReadinessChecker {
@PostConstruct
public void checkReadiness() {
// Verify external dependencies at startup
// Fail fast if any critical dependency is unavailable
verifyDatabaseConnectivity();
verifyCacheConnectivity();
}
}
@Component
@Lazy(false)
public class SecurityPolicyEnforcer {
@PostConstruct
public void enforcePolicy() {
// Security checks must happen at startup — not on first request
validateSecretKeyStrength();
verifyTlsCertificates();
}
}
// Beans that should always be eager even under global lazy:
// - Database migration runners (Flyway, Liquibase)
// - Security configuration validators
// - Application readiness/health checks
// - Scheduled task starters
// - Message consumer initializers (Kafka, RabbitMQ)
// - Cache warmup servicesLazy Initialization in Development
Lazy initialization is particularly valuable during development where fast restarts are more important than first-request performance. Spring Boot DevTools pairs naturally with lazy initialization.
Java
# application-dev.properties — enable lazy init in development only:
spring.main.lazy-initialization=true
# DevTools restart takes 1-2 seconds instead of 5-10 seconds
# First browser request after restart initializes needed beans — acceptable in dev
# application-prod.properties — eager in production:
spring.main.lazy-initialization=false
# All beans created at startup — errors caught before serving traffic
// Measuring startup time improvement:
// Enable startup logging to see which beans are slow to initialize:
// application.properties:
// logging.level.org.springframework.boot.autoconfigure=DEBUG
// Spring Boot Actuator startup endpoint (Boot 2.4+):
// GET /actuator/startup
// Shows the time taken by each startup step including bean initialization
// management.endpoint.startup.enabled=true
// management.endpoints.web.exposure.include=startup
// Identify slow beans:
@Component
public class StartupTimeLogger implements ApplicationListener<ApplicationReadyEvent> {
private final ApplicationStartup applicationStartup;
public StartupTimeLogger(ApplicationStartup applicationStartup) {
this.applicationStartup = applicationStartup;
}
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
// Log total startup time:
long startupMs = event.getTimeTaken().toMillis();
System.out.println("Application started in " + startupMs + "ms");
}
}
// Configure detailed startup metrics (Spring Boot 2.4+):
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(Application.class);
// Track startup steps with timing:
app.setApplicationStartup(new BufferingApplicationStartup(2048));
app.run(args);
}
}Lazy Initialization Trade-offs and Pitfalls
Lazy initialization has real trade-offs beyond just startup time. Understanding these pitfalls prevents subtle production bugs.
Java
// PITFALL 1 — Configuration errors surface late:
@Service
@Lazy
public class PaymentService {
public PaymentService(@Value("${stripe.api.key}") String apiKey) {
if (apiKey == null || apiKey.isBlank()) {
throw new IllegalStateException("Stripe API key not configured");
}
// This error occurs on FIRST USE in production — not at startup
// Could be hours or days after deployment before it's discovered
}
}
// FIX — validate configuration at startup even for lazy beans:
@Component
@Lazy(false) // eager validation
public class PaymentServiceConfigValidator {
@PostConstruct
public void validate(@Value("${stripe.api.key}") String apiKey) {
if (apiKey == null || apiKey.isBlank()) {
throw new IllegalStateException("Stripe API key not configured");
}
}
}
// PITFALL 2 — Circular dependencies hidden until runtime:
@Service
@Lazy
public class ServiceA {
@Autowired ServiceB serviceB;
}
@Service
@Lazy
public class ServiceB {
@Autowired ServiceA serviceA;
}
// With eager initialization: BeanCurrentlyInCreationException at startup
// With lazy initialization: circular dependency discovered only when
// ServiceA or ServiceB is first accessed — could be in production under load
// PITFALL 3 — Thread safety during first initialization:
// When two concurrent requests trigger the first creation of the same lazy bean,
// Spring handles this correctly — only one instance is created (synchronized)
// But @PostConstruct methods must be thread-safe if called during high load
// PITFALL 4 — Health checks pass but beans fail:
// /actuator/health returns UP at startup (no beans initialized yet)
// First real request fails because a lazy bean cannot be created
// FIX: add readiness probes that exercise critical paths after startup:
@Component
public class CriticalPathHealthIndicator implements HealthIndicator {
@Lazy
private final PaymentService paymentService;
public CriticalPathHealthIndicator(@Lazy PaymentService paymentService) {
this.paymentService = paymentService;
}
@Override
public Health health() {
try {
// Forces initialization of lazy PaymentService during health check:
paymentService.ping();
return Health.up().build();
} catch (Exception e) {
return Health.down().withException(e).build();
}
}
}
// WHEN TO USE LAZY (summary):
// - Development environment: YES — fast restarts
// - Batch/admin beans rarely used: YES — avoid startup cost
// - Beans with expensive initialization not on critical path: YES
// - All beans globally in production: CAUTION — use with explicit eager overrides
// - Critical path beans (auth, payment, DB): NO — must be eagerLazy Initialization with @Scope Prototype
Prototype-scoped beans are always lazy by nature — they are created on every request. The interaction between @Lazy and prototype scope in singleton beans requires special handling.
Java
// Prototype beans are inherently lazy — created on every request:
@Component
@Scope("prototype")
public class ReportBuilder {
private final List<ReportSection> sections = new ArrayList<>();
private String title;
public void setTitle(String title) { this.title = title; }
public void addSection(ReportSection section) { sections.add(section); }
public Report build() { return new Report(title, sections); }
}
// Injecting prototype into singleton — requires special handling:
@Service
public class ReportService {
// ObjectProvider gives a new prototype instance on each call:
private final ObjectProvider<ReportBuilder> reportBuilderProvider;
public ReportService(ObjectProvider<ReportBuilder> reportBuilderProvider) {
this.reportBuilderProvider = reportBuilderProvider;
}
public Report generateReport(Long orderId, ReportType type) {
// Fresh ReportBuilder instance for each report:
ReportBuilder builder = reportBuilderProvider.getObject();
builder.setTitle("Order Report #" + orderId);
builder.addSection(fetchOrderSection(orderId));
builder.addSection(fetchPaymentSection(orderId));
return builder.build();
}
}
// @Lazy on singleton injecting prototype — creates a proxy:
@Service
public class AlternativeReportService {
@Autowired
@Lazy
private ReportBuilder reportBuilder; // WRONG — prototype with singleton proxy
// Every call goes through the proxy, but the proxy holds ONE prototype instance
// This defeats the purpose of prototype scope
// USE ObjectProvider, ApplicationContext.getBean(), or @Lookup instead
}
// @Lookup — most elegant solution for prototype in singleton:
@Service
public abstract class ElegantReportService {
@Lookup
protected abstract ReportBuilder createReportBuilder(); // Spring overrides this
public Report generate(Long orderId) {
ReportBuilder builder = createReportBuilder(); // new prototype each time
builder.setTitle("Report #" + orderId);
return builder.build();
}
}