Spring BootConstructor Injection
Spring Boot

Constructor Injection

Constructor injection is the recommended dependency injection style in Spring. Dependencies are declared as constructor parameters — Spring resolves and provides them when creating the bean. It enables final fields, guarantees all dependencies are present before the object is used, and keeps classes fully testable without a Spring container.

What Is Constructor Injection?

Constructor injection is the practice of declaring all required dependencies as parameters of the class constructor. When Spring creates the bean, it resolves each parameter from the application context and passes them to the constructor. The fully initialized object is then registered as a bean and made available for injection elsewhere. Constructor injection is the recommended approach by the Spring team and the broader Java community for three concrete reasons: dependencies can be declared final (ensuring they're never null and never changed), missing dependencies are detected at startup rather than at runtime, and the class can be instantiated and tested with plain Java — no Spring container, no reflection, no mocking framework required for basic unit tests. The word "injection" refers to the framework passing (injecting) the dependencies in from outside — the class doesn't create them. The word "constructor" refers to the mechanism: the dependencies are passed in through the constructor rather than through setter methods or field assignment.

Basic Constructor Injection

The simplest form — declare dependencies as constructor parameters. Spring automatically detects a single constructor and uses it for injection without requiring @Autowired.
Java
// Basic constructor injection — @Autowired optional with a single constructor:
@Service
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final EmailService emailService;

    // @Autowired is optional when there is exactly one constructor (Spring 4.3+):
    public UserService(UserRepository userRepository,
                       PasswordEncoder passwordEncoder,
                       EmailService emailService) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
        this.emailService = emailService;
    }

    public User createUser(CreateUserRequest request) {
        if (userRepository.existsByEmail(request.email())) {
            throw new DuplicateEmailException(request.email());
        }
        User user = new User(
            request.email(),
            passwordEncoder.encode(request.password()),
            request.name()
        );
        User saved = userRepository.save(user);
        emailService.sendWelcome(saved.getEmail(), saved.getName());
        return saved;
    }
}

// Why the fields are final:
// - Declared final → guaranteed non-null after construction
// - Cannot be reassigned after construction → no accidental overwrite
// - JVM guarantees visibility across threads for final fields
// - Compiler error if you try to use before setting in constructor

// Spring resolves each constructor parameter by type:
// UserRepository → finds the bean implementing UserRepository interface
// PasswordEncoder → finds the bean of type PasswordEncoder
// EmailService → finds the bean implementing EmailService interface

// If no matching bean exists → UnsatisfiedDependencyException at startup
// If multiple matching beans exist → NoUniqueBeanDefinitionException at startup
// Both failures happen at startup — not during a production request at 2am

Constructor Injection with @Autowired

When a class has multiple constructors, @Autowired tells Spring which one to use for injection. For a single constructor, the annotation is optional but can improve readability.
Java
// Multiple constructors — @Autowired marks the injection constructor:
@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final PaymentService paymentService;
    private final InventoryService inventoryService;
    private final EmailService emailService;

    // @Autowired required when multiple constructors exist:
    @Autowired
    public OrderService(OrderRepository orderRepository,
                        PaymentService paymentService,
                        InventoryService inventoryService,
                        EmailService emailService) {
        this.orderRepository = orderRepository;
        this.paymentService = paymentService;
        this.inventoryService = inventoryService;
        this.emailService = emailService;
    }

    // Secondary constructor — for testing convenience (no @Autowired):
    public OrderService(OrderRepository orderRepository,
                        PaymentService paymentService) {
        this(orderRepository, paymentService,
             new NoOpInventoryService(),
             new NoOpEmailService());
    }
}

// @Autowired on constructor vs field:
@Service
public class ComparisonExample {

    // Constructor injection (CORRECT):
    private final UserRepository repository;

    @Autowired
    public ComparisonExample(UserRepository repository) {
        this.repository = repository;
        // repository is guaranteed non-null here
        // Can validate: Objects.requireNonNull(repository, "repository must not be null");
    }

    // Field injection (AVOID in production):
    // @Autowired
    // private UserRepository repository;   // not final, null until Spring injects it
}

// Explicit @Autowired is a useful signal in code review:
// It clearly marks "Spring manages this dependency"
// Some teams require it even for single-constructor classes
// for documentation and clarity purposes

Lombok @RequiredArgsConstructor

Lombok's @RequiredArgsConstructor generates a constructor for all final fields, eliminating constructor boilerplate entirely. This is the most common pattern in modern Spring Boot applications.
Java
// Without Lombok — constructor boilerplate for 5 dependencies:
@Service
public class ProductService {

    private final ProductRepository productRepository;
    private final CategoryRepository categoryRepository;
    private final PricingService pricingService;
    private final InventoryService inventoryService;
    private final ImageService imageService;

    public ProductService(ProductRepository productRepository,
                          CategoryRepository categoryRepository,
                          PricingService pricingService,
                          InventoryService inventoryService,
                          ImageService imageService) {
        this.productRepository = productRepository;
        this.categoryRepository = categoryRepository;
        this.pricingService = pricingService;
        this.inventoryService = inventoryService;
        this.imageService = imageService;
    }
}

// With Lombok @RequiredArgsConstructor — same result, zero boilerplate:
@Service
@RequiredArgsConstructor   // generates constructor for all final fields
public class ProductService {

    private final ProductRepository productRepository;
    private final CategoryRepository categoryRepository;
    private final PricingService pricingService;
    private final InventoryService inventoryService;
    private final ImageService imageService;

    // Constructor is generated at compile time — identical to the verbose version above
    // Spring finds the generated constructor and uses it for injection
}

// @RequiredArgsConstructor rules:
// - Generates constructor for ALL fields marked final
// - Also includes fields marked @NonNull (adds null check in constructor body)
// - Fields NOT marked final are NOT included

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;         // included in constructor
    private final PasswordEncoder passwordEncoder;       // included in constructor

    @NonNull
    private EmailService emailService;                   // included + null check

    private String cachedValue;                          // NOT included — not final
    private int requestCount = 0;                        // NOT included — not final

    @Autowired(required = false)
    private MetricsCollector metricsCollector;           // NOT included — not final
}

// Add to pom.xml:
// groupId: org.projectlombok
// artifactId: lombok
// optional: true
// Also add lombok annotation processor for compilation

Constructor Injection and Testability

The defining advantage of constructor injection is testability. A class with constructor-injected dependencies can be instantiated with plain Java — passing real implementations or mocks directly to the constructor, with no Spring context required.
Java
// The service under test:
@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepository;
    private final PaymentService paymentService;
    private final EmailService emailService;
    private final InventoryService inventoryService;

    public Order createOrder(CreateOrderRequest request) {
        // Check inventory
        if (!inventoryService.isAvailable(request.productId(), request.quantity())) {
            throw new InsufficientInventoryException(request.productId());
        }
        // Charge payment
        PaymentResult payment = paymentService.charge(
            request.customerId(), request.totalAmount()
        );
        // Save order
        Order order = new Order(request, payment.getTransactionId());
        Order saved = orderRepository.save(order);
        // Send confirmation
        emailService.sendOrderConfirmation(saved);
        return saved;
    }
}

// UNIT TEST — no Spring, no @SpringBootTest, no application context:
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    // Mocks created by Mockito:
    @Mock private OrderRepository orderRepository;
    @Mock private PaymentService paymentService;
    @Mock private EmailService emailService;
    @Mock private InventoryService inventoryService;

    // Service created with mocks injected via constructor:
    @InjectMocks
    private OrderService orderService;

    @Test
    void createOrder_whenInventoryAvailable_savesAndReturnsOrder() {
        // Arrange
        CreateOrderRequest request = new CreateOrderRequest("PROD-1", 2, "customer-1", new BigDecimal("99.99"));
        when(inventoryService.isAvailable("PROD-1", 2)).thenReturn(true);
        when(paymentService.charge("customer-1", new BigDecimal("99.99")))
            .thenReturn(new PaymentResult("txn-123", PaymentStatus.SUCCESS));
        when(orderRepository.save(any(Order.class)))
            .thenAnswer(inv -> inv.getArgument(0));

        // Act
        Order result = orderService.createOrder(request);

        // Assert
        assertThat(result.getTransactionId()).isEqualTo("txn-123");
        verify(orderRepository).save(any(Order.class));
        verify(emailService).sendOrderConfirmation(any(Order.class));
    }

    @Test
    void createOrder_whenInventoryUnavailable_throwsException() {
        CreateOrderRequest request = new CreateOrderRequest("PROD-1", 100, "customer-1", new BigDecimal("999.99"));
        when(inventoryService.isAvailable("PROD-1", 100)).thenReturn(false);

        assertThatThrownBy(() -> orderService.createOrder(request))
            .isInstanceOf(InsufficientInventoryException.class);

        // Payment and repository never called:
        verifyNoInteractions(paymentService, orderRepository, emailService);
    }
}

// Manual construction — no mocking framework needed for simple cases:
class ManualConstructionTest {

    @Test
    void simpleTest() {
        // Create real or stub implementations directly:
        UserRepository stubRepo = new InMemoryUserRepository();
        PasswordEncoder encoder = new BCryptPasswordEncoder();
        EmailService mockEmail = new NoOpEmailService();

        UserService service = new UserService(stubRepo, encoder, mockEmail);

        User created = service.createUser(new CreateUserRequest("alice@test.com", "Alice", "pass1234"));
        assertThat(created.getEmail()).isEqualTo("alice@test.com");
    }
}

Detecting Design Problems

Constructor injection exposes design problems that other injection styles hide. A constructor with too many parameters is a clear signal that the class violates the Single Responsibility Principle — it's trying to do too much.
Java
// Constructor with 8 parameters — a red flag:
@Service
public class GodService {

    public GodService(
        UserRepository userRepository,
        OrderRepository orderRepository,
        ProductRepository productRepository,
        PaymentService paymentService,
        EmailService emailService,
        SmsService smsService,
        InventoryService inventoryService,
        AuditService auditService
    ) { ... }
    // 8 dependencies = 8 responsibilities = should be split
}

// With field injection, this problem is hidden — it's easy to keep
// adding @Autowired fields without noticing:
@Service
public class HiddenGodService {
    @Autowired private UserRepository userRepository;
    @Autowired private OrderRepository orderRepository;
    @Autowired private ProductRepository productRepository;
    @Autowired private PaymentService paymentService;
    @Autowired private EmailService emailService;
    @Autowired private SmsService smsService;
    @Autowired private InventoryService inventoryService;
    @Autowired private AuditService auditService;
    // Easy to add more — no friction. Problem grows silently.
}

// Fix — split by responsibility:
@Service
@RequiredArgsConstructor
public class OrderCreationService {
    private final OrderRepository orderRepository;
    private final PaymentService paymentService;
    private final InventoryService inventoryService;
    // 3 dependencies — focused responsibility
}

@Service
@RequiredArgsConstructor
public class OrderNotificationService {
    private final EmailService emailService;
    private final SmsService smsService;
    private final AuditService auditService;
    // 3 dependencies — focused responsibility
}

// General guideline:
// 1-3 dependencies: well-focused class
// 4-5 dependencies: acceptable, review carefully
// 6+ dependencies: strong signal to refactor — split the class

// Constructor injection enforces this discipline naturally —
// a 10-parameter constructor is obviously wrong
// 10 @Autowired fields are easy to overlook

Constructor Injection with @Value and @ConfigurationProperties

Configuration values are also injected through the constructor — using @Value for individual properties or @ConfigurationProperties-bound objects for groups of related settings.
Java
// @Value in constructor parameters:
@Service
public class JwtTokenService {

    private final String secret;
    private final long expirationSeconds;
    private final String issuer;

    public JwtTokenService(
            @Value("${app.jwt.secret}") String secret,
            @Value("${app.jwt.expiration:3600}") long expirationSeconds,
            @Value("${app.jwt.issuer:https://myapp.com}") String issuer) {
        this.secret = secret;
        this.expirationSeconds = expirationSeconds;
        this.issuer = issuer;
    }

    public String generateToken(String userId) {
        return Jwts.builder()
            .setSubject(userId)
            .setIssuer(issuer)
            .setExpiration(new Date(System.currentTimeMillis() + expirationSeconds * 1000))
            .signWith(Keys.hmacShaKeyFor(secret.getBytes()))
            .compact();
    }
}

// @ConfigurationProperties object injected as a whole (PREFERRED for groups):
@ConfigurationProperties(prefix = "app.stripe")
public record StripeProperties(
    String apiKey,
    String webhookSecret,
    boolean testMode,
    int maxRetries
) { }

@Service
@RequiredArgsConstructor
public class StripePaymentService implements PaymentService {

    private final StripeProperties stripeProperties;
    private final OrderRepository orderRepository;

    public PaymentResult charge(String customerId, BigDecimal amount) {
        Stripe.apiKey = stripeProperties.apiKey();
        // use stripeProperties.testMode(), stripeProperties.maxRetries(), etc.
        return processStripeCharge(customerId, amount);
    }
}

// application.properties:
// app.stripe.api-key=sk_live_abc123
// app.stripe.webhook-secret=whsec_xyz
// app.stripe.test-mode=false
// app.stripe.max-retries=3

// Testing — inject test configuration directly in constructor:
@Test
void chargesWithCorrectApiKey() {
    StripeProperties testProps = new StripeProperties(
        "sk_test_abc", "whsec_test", true, 1
    );
    OrderRepository mockRepo = mock(OrderRepository.class);
    StripePaymentService service = new StripePaymentService(testProps, mockRepo);
    // No application.properties needed in unit tests — values passed directly
}