Dependency Injection in Spring Boot
Constructor injection, circular dependencies, and advanced Spring Boot DI scenarios with concrete code patterns.
Dependency Injection (DI) isn’t just a buzzword in Spring Boot-it’s the architectural glue that enables modular, testable and scalable code. While most developers understand the basics, senior engineers need to grasp advanced scenarios like circular dependencies, bean lifecycle management and performance optimizations. This article cuts through the noise, focusing on practical mastery of DI’s nuances in real-world Spring Boot applications.
Why Dependency Injection Matters in Spring Boot
Spring Boot’s DI framework shifts responsibility for object creation and wiring from developers to the IoC (Inversion of Control) container. This means:
- Loose Coupling: Components depend on abstractions, not concrete implementations.
- Testability: Dependencies can be mocked or stubbed effortlessly.
- Maintainability: Changes propagate cleanly across the codebase.
But to get these benefits at scale, you need to go beyond `@Autowired`. Here is the deeper cut.
The Three Faces of Dependency Injection: Constructor, Setter and Field
1. What Is Constructor Injection?
Constructor Injection is a DI technique where dependencies are provided via a class’s constructor. The Spring IoC container automatically resolves and injects these dependencies when creating the bean.
Why It’s the Gold Standard
- Immutability: Dependencies are declared as
finalfields, ensuring they’re set once and never modified. - Mandatory Dependencies: Guarantees that the class cannot be instantiated without its required dependencies.
- Testability: Simplifies unit testing-no need for reflection or Spring context to inject mocks.
How to Implement It
public class OrderService { // Declare dependencies as final private final PaymentGateway paymentGateway; private final InventoryService inventoryService; // Single constructor: Spring automatically injects dependencies here public OrderService(PaymentGateway paymentGateway, InventoryService inventoryService) { this.paymentGateway = paymentGateway; this.inventoryService = inventoryService; } }
Key Scenarios for Constructor Injection
- Mandatory Dependencies: Use when a class cannot function without specific collaborators (e.g., a service needing a repository).
- Thread Safety: Immutable dependencies prevent race conditions in multi-threaded environments.
Best Practices
- Avoid
@Autowired: In Spring Boot 4.3+, a single constructor doesn’t require@Autowired-it’s inferred. - Order Dependencies Logically: Place critical dependencies first in the constructor signature.
2. Setter Injection: Flexibility with Caution
What Is Setter Injection?
Dependencies are injected via setter methods (e.g., setPaymentGateway()). Unlike constructor injection, dependencies are optional and mutable.
When to Use It
- Optional Dependencies: Rare cases where a bean can operate without a dependency (e.g., a fallback mechanism).
- Reconfigurable Beans: Dynamically swapping dependencies at runtime (e.g., feature toggles).
Implementation Example
public class NotificationService { private EmailSender emailSender; // Setter method with @Autowired @Autowired public void setEmailSender(EmailSender emailSender) { this.emailSender = emailSender; } }
Risks of Setter Injection
- Temporal Coupling: The class may be instantiated without its dependencies, leading to
NullPointerExceptions. - Mutability: Setters allow dependencies to change post-initialization, risking inconsistent states.
3. Field Injection: A Legacy Antipattern
What Is Field Injection?
Dependencies are injected directly into fields using @Autowired, bypassing constructors or setters.
Why It’s Discouraged
- Hidden Dependencies: Fields are private, making dependencies invisible to the class’s public API.
- Testability Nightmare: Requires Spring context or reflection to inject mocks in tests.
- Immutability Impossible: Fields can’t be
final, allowing unintended modifications.
Example (Not Recommended)
@Service public class ReportService { @Autowired // Avoid this! private DataFormatter formatter; }
When (If Ever) to Use It
- Legacy Code: Only in existing codebases where refactoring isn’t feasible.
- Prototyping: Temporary experiments-never in production.
Use Case: Mandatory dependencies.
Why It Wins:
- Enforces immutability (fields marked
final). - Eliminates
NullPointerExceptionsby ensuring dependencies are set at instantiation. - Simplifies testing (no reflection hacks).
public class OrderService { private final PaymentGateway paymentGateway; // Spring automatically injects the dependency here public OrderService(PaymentGateway paymentGateway) { this.paymentGateway = paymentGateway; } }
2. Setter Injection: For Optional Dependencies
Use Case: Rare scenarios with optional or reconfigurable dependencies.
Pitfalls:
- Breaks immutability.
- Allows incomplete object states if setters aren’t called.
public class CachingService { private CacheManager cacheManager; // Setter isn't called automatically unless you use @Autowired @Autowired public void setCacheManager(CacheManager cacheManager) { this.cacheManager = cacheManager; } }
3. Field Injection: Proceed with Caution
Why It’s Discouraged:
- Hides dependencies (they’re not visible in the public API).
- Tightly couples code to Spring (hard to instantiate classes manually).
- Breaks immutability.
public class ReportService { @Autowired // Avoid this! private DataFormatter formatter; }
Key Takeaway: Prefer constructor injection for 90% of use cases.
Bean Scopes in Real-World Applications
Singleton vs. Prototype: Choosing Wisely
- Singleton (Default): One instance per Spring container. Ideal for stateless services (e.g., utilities, repositories).
- Prototype: New instance every time the bean is requested. Use for stateful objects (e.g., user sessions, request processors).
@Component public class ShoppingCart { // Stateful: Holds items for a specific user private List<Item> items = new ArrayList<>(); }
Web-Aware Scopes: Request, Session and Beyond
- @RequestScope: Beans tied to an HTTP request (e.g., request metadata).
- @SessionScope: Beans tied to a user session (e.g., authentication context).
@Component public class ApiRequestContext { private String clientId; private String authToken; }
Thread Safety
Problem: Singleton beans with mutable state can cause race conditions in multi-threaded environments (e.g., web apps).
Solution:
- Avoid mutable fields in singletons.
- Use
ThreadLocalor prototype scope for stateful operations.
Circular Dependencies
How Circular Dependencies Happen
Bean A → Depends on → Bean B → Depends on → Bean A.
Spring throws BeanCurrentlyInCreationException by default (post-Spring Boot 2.6).
Fixes That Actually Work
- Refactor with a Third Bean:
Extract shared logic into Bean C, breaking the cycle. - Use
@Lazy:
Delay initialization of one bean.@Service public class OrderService { private final PaymentService paymentService; // Delay PaymentService initialization public OrderService(@Lazy PaymentService paymentService) { this.paymentService = paymentService; } }
- Switch to Constructor Injection:
Breaks cycles by enforcing clear dependency order.
Nuclear Option (Not Recommended):
Dependency Injection Patterns for Clean Architecture
1. Strategy Pattern: Swapping Implementations Cleanly
Define an interface, inject multiple implementations, and use @Qualifier to choose:
void processPayment(double amount); } @Component("creditCardStrategy") public class CreditCardStrategy implements PaymentStrategy { /* ... */ } @Component("paypalStrategy") public class PayPalStrategy implements PaymentStrategy { /* ... */ } @Service public class PaymentService { private final PaymentStrategy strategy; // Inject a specific strategy public PaymentService(@Qualifier("creditCardStrategy") PaymentStrategy strategy) { this.strategy = strategy; } }
2. Factory Pattern with @Bean Methods
Create complex objects conditionally:
public class FactoryConfig { @Bean @ConditionalOnProperty(name = "notification.enabled", havingValue = "true") public NotificationService notificationService() { return new AwsSmsService(); } }
3. Adapter Pattern: Integrating Legacy Code
Wrap non-Spring components into Spring-managed beans:
public class LegacyAdapter { @Bean public ThirdPartyLogger thirdPartyLogger() { return new ThirdPartyLoggerAdapter(); } }
Performance Tuning: Eager vs Lazy Initialization
Eager (Default)
- Pros: Fail-fast (issues surface at startup).
- Cons: Slower startup times.
@Lazy Initialization
- Pros: Faster startup for rarely used beans.
- Cons: Runtime delays when the bean is first accessed.
When to Use @Lazy:
- Large beans (e.g., data connectors).
- Beans used only in specific profiles.
@Service public class HeavyReportGenerator { /* ... */ }
Testing: DI in Unit and Integration Tests
1. Mocking Dependencies with @MockBean
Replace real beans with mocks in tests:
public class OrderServiceTest { @MockBean private PaymentGateway paymentGateway; // Replaces the real bean @Autowired private OrderService orderService; }
2. Testing Configuration Classes
Verify that beans are wired correctly:
public class AppConfigTest { @Autowired private DataSource dataSource; @Test void testDataSourceIsConfigured() { assertThat(dataSource).isInstanceOf(HikariDataSource.class); } }
Pitfalls and How to Avoid Them
1. Overusing @ComponentScan
Problem: Scanning too many packages slows startup.
Fix: Explicitly define scan paths:
2. Ignoring Bean Lifecycle Hooks
Solution: Use @PostConstruct for setup logic:
public void init() { // Validate config or preload data }
3. Misusing @Primary and @Qualifier
@Primary: Marks a default bean when multiple candidates exist.@Qualifier: Selects a specific bean by name.
Frequently Asked Questions (FAQs)
@Lazy to delay initialization, or switching to constructor injection.@Lazy for large or rarely used beans to improve startup performance, but be cautious of runtime delays when the bean is first accessed.Mastering dependency injection in Spring Boot isn’t about memorizing annotations for your web services, it’s about designing systems that are resilient, testable, and performant. As a senior developer, focus on:
- Intentional Design: Use constructor injection, avoid circular dependencies.
- Strategic Bean Management: Leverage scopes and conditions.
- Rigorous Testing: Validate wiring and edge cases.
By embracing these principles, you’ll transform DI from a framework feature into a cornerstone of clean architecture. Happy coding!
Related reading: Java OOP principles, Java classes and objects, PostgreSQL thesis, and Java latency optimization.

Skim rule: jump to the section we were stuck on. Kept it for the constructor injection section alone.
Beans and wiring explained without the usual fog. Clearest DI primer we have used in onboarding this year.
Closer to production habits than conference talk tracks. We finally have a stance on constructor injection.
Sounds like how people talk in real reviews. spring dependency injection matched last sprint’s demo pain.
[…] reading: Dependency Injection in Spring Boot, Java OOP principles, and […]
[…] Related reading: Technical SEO 2026, PostgreSQL notes, and Spring Boot DI. […]