Spring BootLazy Initialization
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 directly

Global 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 services

Lazy 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 3Thread 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 eager

Lazy 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();
    }
}