Dependency Injection
Why Dependency Injection Matters
Dependency Injection (DI) decouples object creation from object usage, enabling testability, maintainability, and flexibility. In production systems handling millions of requests, Spring’s IoC container manages thousands of beans with complex dependency graphs, lifecycle hooks, and scope management—manual wiring becomes impossible at scale.
Java Standard Library Baseline
Manual dependency management requires explicit object construction everywhere:
// => ZakatCalculator needs PriceConverter to calculate zakat values
public class ZakatCalculator {
private final PriceConverter priceConverter; // => Dependency
public ZakatCalculator() {
// => Hard-coded dependency: tightly coupled to GoldPriceConverter
// => Can't swap implementations without changing this class
this.priceConverter = new GoldPriceConverter();
}
// => Calculates zakat (Islamic charity) based on gold value
public BigDecimal calculateZakat(BigDecimal goldGrams) {
// => Converts gold grams to monetary value
BigDecimal value = priceConverter.convertToMoney(goldGrams);
// => Zakat is 2.5% of total value above threshold (nisab)
BigDecimal nisab = new BigDecimal("85"); // => 85 grams gold threshold
if (goldGrams.compareTo(nisab) >= 0) {
return value.multiply(new BigDecimal("0.025")); // => 2.5%
}
return BigDecimal.ZERO; // => Below threshold: no zakat
}
}
// => Client code must know all construction details
public class ZakatService {
public void processZakat(BigDecimal gold) {
// => Creates new calculator every time
// => No reuse, no caching, can't control lifecycle
ZakatCalculator calculator = new ZakatCalculator();
BigDecimal zakat = calculator.calculateZakat(gold);
System.out.println("Zakat due: " + zakat);
}
}Limitations:
- Testing: Can’t replace
GoldPriceConverterwith mock for unit tests - Flexibility: Changing price converter requires modifying
ZakatCalculator - Lifecycle: No control over when calculator created/destroyed
- Reuse: Creates new instances repeatedly instead of reusing
Spring Core Solution
Spring’s IoC container manages bean creation, injection, and lifecycle:
// => Configuration class: central location for bean definitions
@Configuration // => Marks class as source of bean definitions
// => Spring scans for @Bean methods during startup
public class ZakatConfig {
@Bean // => Registers PriceConverter bean in Spring container
// => Method name becomes bean name: "priceConverter"
public PriceConverter priceConverter() {
// => Spring calls this method once (singleton scope default)
// => Instance stored in container, reused for all injections
return new GoldPriceConverter();
}
@Bean // => Registers ZakatCalculator bean
// => Parameter priceConverter: Spring injects the bean automatically
public ZakatCalculator zakatCalculator(PriceConverter priceConverter) {
// => Spring resolved dependency: finds priceConverter bean by type
// => Constructor injection: dependency passed to constructor
return new ZakatCalculator(priceConverter);
}
}
// => Service class: dependencies injected, no manual construction
public class ZakatCalculator {
private final PriceConverter priceConverter; // => Dependency injected by Spring
// => Constructor accepts dependency: enables testing with mocks
public ZakatCalculator(PriceConverter priceConverter) {
this.priceConverter = priceConverter;
}
public BigDecimal calculateZakat(BigDecimal goldGrams) {
BigDecimal value = priceConverter.convertToMoney(goldGrams); // => Uses injected converter
BigDecimal nisab = new BigDecimal("85");
if (goldGrams.compareTo(nisab) >= 0) {
return value.multiply(new BigDecimal("0.025"));
}
return BigDecimal.ZERO;
}
}
// => Client code: Spring manages construction
@Service // => Marks class as Spring-managed service bean
public class ZakatService {
private final ZakatCalculator zakatCalculator; // => Injected by Spring
// => Constructor injection: Spring finds zakatCalculator bean and injects it
public ZakatService(ZakatCalculator zakatCalculator) {
this.zakatCalculator = zakatCalculator;
}
public void processZakat(BigDecimal gold) {
// => Uses injected calculator: no manual construction
// => Same instance reused across all calls (singleton)
BigDecimal zakat = zakatCalculator.calculateZakat(gold);
System.out.println("Zakat due: " + zakat);
}
}
// => Application startup: Spring wires everything
public class Application {
public static void main(String[] args) {
// => Creates Spring container from configuration class
// => Scans @Bean methods, creates beans, injects dependencies
ApplicationContext context = new AnnotationConfigApplicationContext(ZakatConfig.class);
// => Retrieves fully-wired service from container
// => All dependencies already injected by Spring
ZakatService service = context.getBean(ZakatService.class);
service.processZakat(new BigDecimal("100"));
}
}Benefits:
- Testability: Replace
priceConverterbean with mock in tests - Flexibility: Change
@Beanmethod to return different implementation - Lifecycle: Spring manages creation (startup) and destruction (shutdown)
- Reuse: Singleton scope (default) ensures single instance reused
Progression Diagram
graph TD
A[Manual Construction<br/>new Dependencies] -->|Tight Coupling| B[Hard to Test]
A -->|Spread Across Code| C[Hard to Maintain]
D[Spring DI<br/>@Configuration + @Bean] -->|Loose Coupling| E[Easy to Test]
D -->|Centralized Config| F[Easy to Maintain]
D -->|Container Managed| G[Lifecycle Hooks]
style A fill:#DE8F05,stroke:#333,stroke-width:2px,color:#fff
style D fill:#029E73,stroke:#333,stroke-width:2px,color:#fff
style E fill:#0173B2,stroke:#333,stroke-width:2px,color:#fff
style F fill:#0173B2,stroke:#333,stroke-width:2px,color:#fff
style G fill:#0173B2,stroke:#333,stroke-width:2px,color:#fff
Production Patterns
Constructor Injection (Recommended)
@Service
public class OrderService {
private final PaymentService paymentService; // => final: immutable after construction
private final NotificationService notificationService;
// => Constructor injection: all dependencies required, explicit
// => Spring automatically injects when only one constructor exists
public OrderService(PaymentService paymentService,
NotificationService notificationService) {
this.paymentService = paymentService;
this.notificationService = notificationService;
}
}Benefits:
- Dependencies explicit in constructor signature
finalfields prevent accidental reassignment- Fails at startup if dependencies missing (fail-fast)
- Easy testing: pass mocks to constructor
Setter Injection (Rare)
@Service
public class ReportGenerator {
private EmailService emailService; // => Optional dependency
@Autowired(required = false) // => Optional: doesn't fail if bean missing
public void setEmailService(EmailService emailService) {
this.emailService = emailService; // => Injected if available
}
public void generate(Report report) {
// => Check if optional dependency present
if (emailService != null) {
emailService.send(report);
}
}
}Use when: Dependency truly optional (feature works without it)
Field Injection (Avoid)
@Service
public class OrderService {
@Autowired // => AVOID: hides dependencies, harder to test
private PaymentService paymentService;
}Problems: Can’t create instance without Spring, dependencies hidden, can’t use final
Circular Dependency Detection
@Service
public class ServiceA {
private final ServiceB serviceB;
public ServiceA(ServiceB serviceB) { // => A depends on B
this.serviceB = serviceB;
}
}
@Service
public class ServiceB {
private final ServiceA serviceA;
public ServiceB(ServiceA serviceA) { // => B depends on A
this.serviceA = serviceA; // => Circular: A → B → A
}
}
// => Spring detects circular dependency at startup
// => BeanCurrentlyInCreationException thrownSolution: Extract shared logic to third service or use @Lazy (not recommended)
Qualifier for Multiple Implementations
public interface PaymentService {
void charge(BigDecimal amount);
}
@Service("creditCardPayment") // => Bean name: creditCardPayment
public class CreditCardPaymentService implements PaymentService {
public void charge(BigDecimal amount) {
// => Charge credit card
}
}
@Service("paypalPayment") // => Bean name: paypalPayment
public class PayPalPaymentService implements PaymentService {
public void charge(BigDecimal amount) {
// => Charge PayPal
}
}
@Service
public class OrderService {
private final PaymentService paymentService;
// => @Qualifier specifies which bean to inject
public OrderService(@Qualifier("creditCardPayment") PaymentService paymentService) {
this.paymentService = paymentService; // => Injects CreditCardPaymentService
}
}Trade-offs and When to Use
| Approach | Learning Curve | Testability | Flexibility | Lifecycle Control |
|---|---|---|---|---|
| Manual Java | Low | Hard | Limited | Manual |
| Spring DI | Medium | Easy | High | Automatic |
| Spring Boot DI | High | Easy | Very High | Automatic + Auto-config |
When to Use Manual Java:
- Simple utilities with zero dependencies
- Performance-critical code (DI adds minimal overhead)
- Learning projects to understand dependency patterns
When to Use Spring DI:
- Enterprise applications with complex dependencies
- Applications requiring testability
- Systems needing lifecycle management (startup hooks, shutdown cleanup)
- When using other Spring modules (Data, Security, Web)
Best Practices
1. Prefer Constructor Injection
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) { // => Recommended
this.paymentService = paymentService;
}
}2. Use @Autowired Only When Necessary
// Single constructor: no @Autowired needed
public OrderService(PaymentService paymentService) { // => Spring auto-detects
}
// Multiple constructors: mark which one Spring should use
@Autowired // => Required when multiple constructors exist
public OrderService(PaymentService paymentService, NotificationService notificationService) {
}3. Make Dependencies Final
private final PaymentService paymentService; // => Immutable, prevents reassignment4. Keep Bean Scopes Consistent
@Service // => Singleton: one instance for entire app (default)
public class OrderService {
// => No mutable state: thread-safe
}
@Component
@Scope("prototype") // => New instance per injection
public class OrderProcessor {
// => Can have mutable state (not shared across threads)
}5. Use Profiles for Environment-Specific Beans
@Configuration
public class DataConfig {
@Bean
@Profile("dev") // => Only created in development environment
public DataSource devDataSource() {
return new H2DataSource(); // => In-memory database for dev
}
@Bean
@Profile("prod") // => Only created in production environment
public DataSource prodDataSource() {
return new PostgresDataSource(); // => Production database
}
}See Also
- Configuration - Java Config patterns
- Bean Lifecycle - Initialization and destruction
- Component Scanning - @Component discovery
- Spring Best Practices - DI patterns
- Java Dependency Injection - Java baseline patterns