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

ConstructorField (@Autowired)
Fields can be finalyes (immutable, thread-safe)no
Object always fully builtyesno, fields are null until Spring sets them
Unit test without Springjust call new CartService(mock)needs reflection or Spring
Too many dependencies visibleyes, a long constructor is a warning signhidden

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.