2.2 Dependency Injection
← Back to Chapter 2: Spring Beans & Dependency Injection
Dependency Injection (DI) is the concrete implementation of Inversion of Control in Spring. Objects receive their dependencies from the framework rather than creating them directly.
What is Dependency Injection?
Dependency Injection means: instead of a class creating the objects it needs, we give those objects to the class from outside.
This makes the code loosely coupled, easier to test, and easier to maintain.
Without Dependency Injection
The class itself creates the dependency:
class UserService {
private UserRepository repository = new UserRepository();
public void getUser() {
repository.findUser();
}
}
The problem is that UserService is tightly connected to the concrete UserRepository.
If later you want to use another implementation, you need to modify UserService.
For example:
UserService
|
↓
new UserRepository()
The UserService is responsible for creating and managing its dependency.
With Dependency Injection
Instead of creating UserRepository, we provide it to UserService.
class UserService {
private UserRepository repository;
// Dependency is injected through constructor
public UserService(UserRepository repository) {
this.repository = repository;
}
public void getUser() {
repository.findUser();
}
}
Now someone else creates UserRepository and gives it to UserService.
creates
↓
UserRepository
|
| inject
↓
UserService
This is Dependency Injection.
Dependency Injection is a design pattern where the dependencies of a class are provided from outside instead of the class creating them itself.
The Three Forms of Injection
Spring supports three primary mechanisms to inject collaborating dependencies into a bean:
1. Constructor Injection (Industry Best Practice) ✅
@Service
public class UserService {
private final UserRepository userRepository;
private final NotificationService notificationService;
// @Autowired is optional in Spring 4.3+ when there is only one constructor
public UserService(UserRepository userRepository, NotificationService notificationService) {
this.userRepository = userRepository;
this.notificationService = notificationService;
}
}
Why constructor injection?
Because the dependency is required for the class to work.
Also:
fields can be final
dependencies are clearly visible
easier unit testing
object cannot be created without required dependencies
2. Setter Injection (For Optional Dependencies)
Dependency is provided through a setter method.
Useful when the dependency is optional or can change later.
@Service
public class ReportService {
private Formatter formatter;
@Autowired
public void setFormatter(Formatter formatter) {
this.formatter = formatter;
}
}
3. Field Injection (Discouraged / Anti-Pattern) ❌
Dependency is injected directly into a field.
@Service
public class BadService {
@Autowired // Avoid!
private PaymentClient paymentClient;
}
It's simple, but generally not preferred because:
harder to unit test
dependencies aren't obvious from the constructor
cannot easily make the field final
Constructor injection is generally preferred over field injection.
Why Field Injection is an Anti-Pattern
Despite being compact, field injection introduces critical architectural problems:
- Breaks Immutability: Fields cannot be declared
final, making the class prone to unwanted state modification. - Hides Coupling & SRP Violations: With constructor injection, a constructor with 8 parameters immediately signals a violation of the Single Responsibility Principle. Field injection hides this smell.
- Hard to Unit Test: Fields cannot be populated in pure unit tests (
new BadService()) without using reflection or heavy Spring testing contexts. - Obscures Circular Dependencies: Field-injected classes may hide circular dependencies that surface only at runtime under specific invocation orders.
Circular Dependencies & Resolution
A circular dependency occurs when Bean A requires Bean B, and Bean B directly or indirectly requires Bean A:
┌────────┐ ┌────────┐
│ Bean A ├──────────────>│ Bean B │
└────────┘ └────────┘
▲ │
└────────────────────────┘
What Happens in Spring Boot?
Starting in Spring Boot 2.6+, circular dependencies are forbidden by default and result in immediate startup failure (BeanCurrentlyInCreationException).
Production Fixes:
- Refactor Architecture (Recommended): Extract shared logic into a distinct third component (
Bean C) that bothAandBconsume. - Event-Driven Decoupling: Replace direct synchronous method invocation with Spring
ApplicationEventPublisher. @LazyInjection (Workaround): Break the initialization cycle by having Spring inject a lazy proxy that initializes on first method call:public UserService(@Lazy AuditService auditService) {this.auditService = auditService;}
DI vs Factory Pattern
These are related but different.
Factory
A factory is responsible for creating an object.
Factory
↓
creates
↓
PaymentService
Dependency Injection
DI is about providing the dependency to another object.
PaymentService
↓
injected into
↓
OrderService
Spring can internally use various mechanisms to create/manage beans, while your application benefits from DI.
Next Up: Learn how Spring automatically discovers and wires dependencies using 2.3 Autowiring.