Understanding @Lazy in Spring: Solving Circular Dependencies and Accelerating Startup
The article explains that Spring's @Lazy annotation has two distinct mechanisms—class‑level lazy bean creation and dependency‑level proxy placeholders—detailing how each works, when to apply them, common pitfalls such as transaction and aspect failures, and best‑practice scenarios for reducing startup time while safely handling circular dependencies.
1. Two Underlying Mechanisms of @Lazy
Spring's @Lazy annotation is not a single function; it has two independent implementations that differ in annotation location, source‑code logic, effect, and failure risk.
1. @Lazy on a class: Bean‑wide lazy instantiation
Spring normally instantiates all singleton beans during container refresh (finishBeanFactoryInitialization). When @Lazy is placed on a class, the container only registers the BeanDefinition during startup; no instance is created, dependencies are not injected, and initialization methods are not executed. The actual bean creation is deferred until the bean is first requested by business code or injected into a non‑lazy bean.
Core capability: only speeds up startup; it cannot resolve circular dependencies.
2. @Lazy on a dependency/constructor parameter: Proxy placeholder for delayed injection
This is the core mechanism for solving circular dependencies and a frequent interview topic. When @Lazy is added to an @Autowired field or constructor parameter, Spring calls getLazyResolutionProxyIfNecessary to generate a CGLIB dynamic proxy placeholder instead of looking up the real bean.
The proxy has two key characteristics:
During host bean creation, the proxy placeholder is injected immediately, allowing the dependency graph to be assembled without waiting for the target bean to be fully initialized.
The proxy holds lazy‑resolution logic; the real bean is fetched from the IoC container only when a business method is invoked.
Core capability: solves constructor‑based circular dependencies; it does not improve startup speed.
Source code evidence
When Spring resolves an autowired dependency, it first checks for @Lazy. If present, it creates a lazy proxy and skips immediate dependency resolution; otherwise it calls getBean() to obtain the real bean, triggering the full creation flow.
This explains why field/constructor @Lazy can break circular dependencies while class‑level @Lazy cannot.
2. Drastically Reducing Startup Time
1. Root causes of slow startup
As business evolves, many heavyweight beans are initialized during startup: MQ consumers/producers, Redis clusters, OSS clients, large local caches, full dictionary pre‑warming, scheduled task initialization, third‑party API pre‑checks, etc. These operations block the container startup sequentially, stretching startup from seconds to dozens of seconds.
2. How @Lazy speeds up startup
Normal beans execute the full lifecycle (instantiation → DI → initialization → caching) during startup, blocking the process. An @Lazy bean registers only its definition at startup, moving all time‑consuming logic to runtime. The service can quickly finish container refresh, listen on ports, and start successfully, greatly improving development and deployment efficiency.
3. Applicable scenarios
✅ Suitable for class‑level @Lazy:
Low‑frequency background tasks, data statistics, report calculations, archival cleanup, heavyweight third‑party clients, non‑core components.
❌ Not suitable for @Lazy:
Core user‑facing interfaces, interceptors, authentication components, global handlers, fundamental utility beans. Using @Lazy here can cause large latency on the first request.
4. Lazy loading + container pre‑warming
To resolve the “fast startup but slow first request” paradox, large companies add @Lazy to heavy components and listen for container refresh events to asynchronously pre‑warm beans in the background, avoiding startup blockage and preserving user experience.
3. The Only Compatible Solution for Constructor‑based Circular Dependencies
1. The fatal limitation of the three‑level cache
The three‑level cache can only resolve field/setter injection circular dependencies. With constructor injection and final fields, the cache cannot help because the bean must be fully constructed before any reference is available, leading to BeanCurrentlyInCreationException.
2. How @Lazy breaks constructor circular dependencies
Scenario: UserService and OrderService depend on each other via constructors.
The container creates UserService and sees that OrderService’s constructor parameter is annotated with @Lazy.
Spring generates a proxy placeholder for OrderService without creating the real bean.
UserService is instantiated, initialized, and stored in the first‑level cache.
The container proceeds to create OrderService, injecting the already‑ready UserService.
The circular loop is fully resolved and startup succeeds.
The proxy loads the real OrderService only when a business method is actually invoked.
3. Comparison with other circular‑dependency solutions
✅ Setter injection: requires abandoning final fields, losing thread‑safety and immutability.
✅ Business refactoring: high migration cost for legacy projects.
✅ Three‑level cache: cannot handle constructor injection.
✅ @DependsOn: forces creation order and adds coupling.
4. Four @Lazy Usage Scenarios Compared
Many developers stumble because they cannot distinguish the four forms.
1. Constructor parameter @Lazy
Retains final, follows best practices, perfectly solves circular dependencies with no code intrusion; preferred by large companies.
2. Field @Lazy
Works but field injection is discouraged due to testing and null‑pointer risks.
3. Setter @Lazy
Suitable for optional dependencies, not for core business circular dependencies.
4. Class‑level @Lazy
Only speeds up startup; does not resolve circular dependencies; 90 % of developers misuse it.
5. Cautions
1. @Lazy proxies can cause transaction and aspect failures
This is the most frequent hidden bug in production. Ordinary bean proxies are created after initialization, but @Lazy creates a placeholder proxy early, disrupting aspect execution order. In scenarios with multiple aspects, transaction propagation, or nested calls, you may see transactions not rolling back, logging aspects not firing, or permission checks being skipped.
Solution: Use @Lazy only as a temporary fix; core business should avoid long‑term reliance and be refactored to eliminate circular dependencies.
2. First‑request latency spikes
Heavy beans annotated with class‑level @Lazy execute all initialization logic on the first request, causing a dramatic time increase and potential time‑outs under load.
Solution: Asynchronously pre‑warm the container after startup to balance startup speed and request performance.
3. Internal this‑calls bypass the proxy and cause NPEs
Lazy proxies affect only external injections; internal this‑calls refer to the raw object, leading to null‑pointer errors.
Solution: Use the injected proxy for internal calls; avoid self‑invocation via this.
4. Overusing @Lazy masks design flaws
Circular dependencies indicate layered or responsibility coupling issues. Relying on @Lazy as a band‑aid accumulates technical debt and inflates future refactoring cost.
5. Prototype beans and utility classes @Lazy are ineffective
Prototype beans have no cache or lifecycle management, so lazy mechanisms do not apply; they cannot speed up startup nor resolve dependencies.
@Lazy appears simple but is a sophisticated part of Spring’s container design; it can solve pain points but has strict usage boundaries.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
