Lazy Initialization
Lazy Initialization
By default, Spring creates and fully wires every singleton bean the moment the ApplicationContext is refreshed — a strategy called eager initialization. This is intentional: it surfaces misconfiguration errors (missing dependencies, bad property values) at startup rather than minutes later when a user hits a rarely-used endpoint. For the vast majority of beans, eager initialization is exactly the right choice.
But "eager by default" is not "eager always". Some beans are expensive to construct, connect to external resources, or are genuinely optional paths that many application instances never exercise. For those cases, Spring offers lazy initialization: the bean is not created until something first asks for it. This lesson covers exactly when that trade-off makes sense, how to apply it, and what surprises to watch for.
Eager vs Lazy — the Core Difference
Consider the two timelines:
- Eager:
ApplicationContextrefresh → all singleton beans constructed, dependencies injected,@PostConstructmethods run → application ready. - Lazy:
ApplicationContextrefresh → bean definition registered but no instance created → first time something requests the bean → construction happens on that request's thread.
The container still validates that the bean definition is well-formed (the class exists, the constructor is unambiguous) at startup. What it defers is the actual new, the dependency wiring, and the lifecycle callbacks.
Declaring a Lazy Bean with @Lazy
Annotate any @Component, @Service, @Repository, or @Bean method with @Lazy and Spring treats it as lazy:
With @Lazy in place, the constructor message will not appear during context startup. It appears only when another bean or a request first calls a method on ReportGeneratorService.
null — the field still needs a value at startup. Instead it injects a CGLIB proxy. The proxy looks and feels like the real bean but delegates every method call to the actual instance, creating it on the first call. This is why @Lazy injection sites on eager beans also need the annotation (see below).
@Lazy on an Injection Point
If an eager bean injects a lazy bean, you must annotate the injection point as well — otherwise Spring instantiates the lazy bean at startup anyway to satisfy the eager dependency:
The proxy Spring inserts is transparent: you call methods on it normally, equality checks work, and it participates correctly in transactions and AOP advice.
Global Lazy Initialization
Spring Boot 2.2+ added a one-line global flag:
This makes every bean lazy by default. It is popular for local development because it dramatically reduces startup time on large applications — only the beans touched by the first request are created. However, deploying it to production is risky: configuration errors that would normally abort startup are instead deferred until the first request reaches the misconfigured bean, potentially returning a 500 to the first real user.
@Lazy(false) to force them eager and keep startup-time validation for the important paths.
When @Lazy Is Genuinely Appropriate
Lazy initialization is not a performance hack to apply everywhere. It is the right tool in specific situations:
- Expensive optional resources: A service that opens a connection to a legacy system used only by the admin reporting module. Most application instances in a horizontally scaled cluster may never exercise it.
- Development / test speed: Global lazy mode slashes startup time during iterative development. Keep the production profile eager.
- Circular dependency workaround (last resort): Breaking a circular dependency with
@Lazycan defer construction enough to resolve the chicken-and-egg problem. This is a smell, though — prefer refactoring to eliminate the cycle. - Optional integrations: A bean that wraps a third-party SDK that may not be configured on all deployment environments. Pair it with
@ConditionalOnPropertyfor cleaner control.
When NOT to Use @Lazy
Also avoid relying on lazy initialization to "fix" long startup times caused by poor bean design. If a bean takes three seconds to initialise, that cost does not disappear — it just moves to the first request, creating a cold-start latency spike. The real fix is to make the initialisation itself cheaper (async, lighter validation, smaller scope).
Prototype Beans and Lazy Initialization
Prototype-scoped beans are inherently lazy — the container never pre-creates them, because there is no single instance to pre-create. Annotating a prototype bean with @Lazy has no additional effect. The behaviour you see in practice:
Verifying Lazy Behaviour
During development you can quickly verify that a bean is truly lazy by watching the startup log. With DEBUG level logging on org.springframework, Spring logs each bean creation. A lazy bean's creation log line will appear after the context-refresh log, tied to the first method invocation rather than to startup. A simple sanity check:
containsBean checks for a registered definition; containsSingleton checks whether a singleton instance has been created. A lazy bean shows true / false until it is first used.
Summary
Eager initialization catches problems at startup and is the right default for almost everything. Use @Lazy selectively — on beans that are expensive, genuinely optional, or only needed on specific code paths. When injecting a lazy bean into an eager one, annotate the injection site with @Lazy too, so Spring installs a proxy rather than forcing early construction. The global spring.main.lazy-initialization=true flag is a productivity aid for local development, not a production optimisation. Keep critical infrastructure beans eager so misconfiguration surfaces at startup, not in front of users.