Careeroza
Constructor Injection vs Autowired
Three Ways to Inject
// 1. Constructor injection (RECOMMENDED)
@Service
public class CartService {
private final ProductService productService;
public CartService(ProductService productService) { // @Autowired is optional with one constructor
this.productService = productService;
}
}
// 2. Setter injection (for optional dependencies)
@Service
public class ReportService {
private EmailSender emailSender;
@Autowired(required = false)
public void setEmailSender(EmailSender emailSender) {
this.emailSender = emailSender;
}
}
// 3. Field injection (avoid in new code)
@Service
public class OldStyleService {
@Autowired
private ProductService productService; // cannot be final, hidden dependency
}Why Constructor Injection Wins
| Constructor | Field (@Autowired) | |
|---|---|---|
Fields can be final | yes (immutable, thread-safe) | no |
| Object always fully built | yes | no, fields are null until Spring sets them |
| Unit test without Spring | just call new CartService(mock) | needs reflection or Spring |
| Too many dependencies visible | yes, a long constructor is a warning sign | hidden |
Example
Constructor injection makes a unit test trivial: no Spring, just new with a fake dependency.
import java.util.Map;
public class Main {
public static void main(String[] args) {
ProductService fakeProducts = new ProductService(Map.of(1L, 1499, 2L, 599));
CartService cart = new CartService(fakeProducts);
int total = cart.total(Map.of(1L, 2, 2L, 1)); // 2 x 1499 + 1 x 599
System.out.println("Cart total = " + total);
System.out.println(total == 3597 ? "TEST PASSED" : "TEST FAILED");
}
}
class ProductService {
private final Map<Long, Integer> prices;
ProductService(Map<Long, Integer> prices) { this.prices = prices; }
int priceOf(long id) { return prices.get(id); }
}
class CartService {
private final ProductService productService; // final: set once
CartService(ProductService productService) { this.productService = productService; }
int total(Map<Long, Integer> qtyByProduct) {
return qtyByProduct.entrySet().stream()
.mapToInt(e -> productService.priceOf(e.getKey()) * e.getValue())
.sum();
}
}Output
Cart total = 3597 TEST PASSED
More Than One Candidate
If two beans implement the same interface, Spring does not know which one to inject and fails with NoUniqueBeanDefinitionException. Fix it with @Primary (the default choice) or @Qualifier (pick by name):
@Component @Primary
class RazorpayGateway implements PaymentGateway { ... }
@Component("cashOnDelivery")
class CodGateway implements PaymentGateway { ... }
@Service
class CheckoutService {
CheckoutService(PaymentGateway online, // gets Razorpay (@Primary)
@Qualifier("cashOnDelivery") PaymentGateway cod) { ... } // gets COD
}Tip: Lombok users often write @RequiredArgsConstructor on the class to generate the constructor for all final fields. Same constructor injection, less typing.