Constructor Injection

22 min read

Object-Oriented Design and SOLID Review — review Java classes for field injection and replace it with constructor injection to make dependencies explicit and testable.

1. Introduction

Constructor Injection is a dependency injection technique where a class receives the objects it depends on through its constructor.

In Java and Spring Boot projects, service classes commonly depend on repositories, API clients, validators, mappers, configuration components, or other services.

Instead of creating those dependencies inside the class or injecting them into mutable fields, Constructor Injection makes the required dependencies explicit when the object is created.

Example:

JAVA
@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentClient paymentClient;
    public OrderService(OrderRepository orderRepository, PaymentClient paymentClient) {
        this.orderRepository = orderRepository;
        this.paymentClient = paymentClient;
    }
}

This approach is especially useful during code reviews because it clearly shows what a class needs in order to work correctly.

Constructor Injection improves:

  • Dependency visibility
  • Testability
  • Immutability of dependency references
  • Class design
  • Failure detection during application startup
  • Maintainability

In modern Spring Boot applications, Constructor Injection is generally the preferred approach for required dependencies.

2. What This Topic Means

A Java class usually cannot perform all work by itself.

For example, an OrderService may need:

  • OrderRepository to access the database
  • PaymentClient to call a payment service
  • InventoryService to reserve products
  • NotificationService to send confirmation messages

These objects are dependencies of OrderService.

With Constructor Injection, the class receives these dependencies when the class instance is created.

Example:

JAVA
public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentClient paymentClient;
    public OrderService(OrderRepository orderRepository, PaymentClient paymentClient) {
        this.orderRepository = orderRepository;
        this.paymentClient = paymentClient;
    }
}

The class does not decide how those dependencies are created.

It only declares:

"I cannot work correctly unless you provide these dependencies."

In a Spring Boot application, the Spring container creates the dependency objects and passes them to the constructor.

For a Spring-managed component with a single constructor, @Autowired normally does not need to be added explicitly.

Example:

JAVA
@Service
public class OrderService {
    private final OrderRepository orderRepository;
    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }
}

Spring detects the constructor automatically and injects the required bean.

3. Why It Matters in Real Projects

Constructor Injection is not merely a syntax preference.

It directly affects how understandable, testable, and maintainable a class becomes.

Readability

The constructor shows every required dependency.

When a developer opens a class such as:

JAVA
public OrderService(
        OrderRepository orderRepository,
        PaymentClient paymentClient,
        InventoryService inventoryService) {
    this.orderRepository = orderRepository;
    this.paymentClient = paymentClient;
    this.inventoryService = inventoryService;
}

the developer immediately understands which collaborators the service requires.

With field injection, dependencies may be scattered throughout the class.

Maintainability

Constructor Injection encourages dependency references to be declared final.

Example:

JAVA
private final OrderRepository orderRepository;

This prevents another method from accidentally replacing the dependency.

Testability

Dependencies can easily be supplied using mocks or stubs without starting Spring.

Example:

JAVA
OrderRepository repository = mock(OrderRepository.class);
PaymentClient paymentClient = mock(PaymentClient.class);
OrderService service = new OrderService(repository, paymentClient);

This makes fast unit testing straightforward.

Debugging

Missing required dependencies usually become visible when the object is created rather than much later when a method happens to access a null field.

Reliability

A successfully created object is more likely to be in a valid state because required dependencies must already exist.

Team Development

Constructor Injection gives reviewers a clear view of the class dependency graph.

A reviewer can quickly notice that a service has accumulated too many responsibilities if the constructor requires ten or fifteen dependencies.

4. Core Concept

The core idea behind Constructor Injection is:

Required dependencies should be supplied when an object is created.

Consider a payment-processing service.

JAVA
@Service
public class PaymentService {
    private final PaymentRepository paymentRepository;
    private final PaymentGatewayClient paymentGatewayClient;
    public PaymentService(
            PaymentRepository paymentRepository,
            PaymentGatewayClient paymentGatewayClient) {
        this.paymentRepository = paymentRepository;
        this.paymentGatewayClient = paymentGatewayClient;
    }
}

The PaymentService cannot exist normally without:

  • PaymentRepository
  • PaymentGatewayClient

Therefore they are constructor parameters.

Dependency Inversion

Constructor Injection works especially well when the class depends on abstractions.

Example:

JAVA
public interface PaymentGateway {
    PaymentResult charge(PaymentRequest request);
}

Implementation:

JAVA
@Component
public class RazorpayPaymentGateway implements PaymentGateway {
    @Override
    public PaymentResult charge(PaymentRequest request) {
        return new PaymentResult(true);
    }
}

Service:

JAVA
@Service
public class PaymentService {
    private final PaymentGateway paymentGateway;
    public PaymentService(PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }
}

The business service depends on the PaymentGateway abstraction instead of manually constructing a particular implementation.

This improves flexibility and testability.

Object Validity

Constructor Injection helps enforce valid object creation.

Bad:

JAVA
PaymentService service = new PaymentService();
service.setPaymentGateway(paymentGateway);

Between those two statements, the object exists without a required dependency.

Constructor-based design avoids that intermediate invalid state.

5. Important Rules

When reviewing Constructor Injection in Java and Spring Boot applications, follow these practical rules:

  • Use Constructor Injection for required dependencies.
  • Declare injected dependencies final where possible.
  • Do not create infrastructure dependencies manually using new inside business services.
  • Avoid field injection for ordinary application dependencies.
  • Do not add @Autowired unnecessarily when a Spring component has only one constructor.
  • Prefer abstractions when multiple implementations or testing flexibility are required.
  • Avoid using constructors with excessive numbers of dependencies.
  • Treat an oversized constructor as a possible Single Responsibility Principle warning.
  • Do not pass unrelated configuration values individually when a configuration object would be clearer.
  • Do not make a dependency nullable unless it is genuinely optional.
  • Avoid hiding dependencies through static utility access or service locators.
  • Keep constructors focused on assigning dependencies rather than executing business logic.
  • Do not perform database calls, API calls, or heavy computation inside constructors.

6. Bad Code Example

Consider the following order-processing service.

JAVA
@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private PaymentClient paymentClient;
    @Autowired
    private NotificationService notificationService;
    public OrderResponse placeOrder(OrderRequest request) {
        Order order = new Order();
        order.setCustomerId(request.getCustomerId());
        order.setAmount(request.getAmount());
        Order savedOrder = orderRepository.save(order);
        PaymentResult paymentResult = paymentClient.processPayment(
                savedOrder.getId(),
                savedOrder.getAmount());
        if (paymentResult.isSuccessful()) {
            notificationService.sendOrderConfirmation(savedOrder.getId());
        }
        return new OrderResponse(savedOrder.getId(), paymentResult.isSuccessful());
    }
}

This code may work correctly when Spring creates the object.

However, the dependency design is weaker than necessary.

7. Problems in the Bad Code

Hidden Dependencies

The dependencies are injected directly into private fields.

A developer looking only at object construction cannot immediately see that OrderService requires three collaborators.

Mutable Dependencies

The fields cannot conveniently be declared final.

Therefore the references are mutable for the lifetime of the object.

Difficult Plain Unit Testing

Creating the service directly is inconvenient.

This does not work correctly:

JAVA
OrderService service = new OrderService();

The dependencies remain uninitialized unless reflection, Spring, or another injection mechanism populates them.

Invalid Object State

The class can theoretically exist without its required dependencies.

If the object is created outside Spring, calling:

JAVA
service.placeOrder(request);

may cause a NullPointerException.

Framework Coupling

The class relies directly on Spring's field-injection behavior.

The business service becomes harder to instantiate as an ordinary Java class.

Reduced Design Visibility

During a Pull Request review, constructor dependencies provide useful architectural information.

Field injection hides some of that information.

8. Code Review Findings

A senior Java developer reviewing the previous code should notice:

  • OrderRepository, PaymentClient, and NotificationService are required dependencies.
  • All three are injected using fields instead of through the constructor.
  • The dependency references are mutable.
  • Plain Java unit testing becomes unnecessarily difficult.
  • The class can be instantiated in an invalid state outside the Spring container.
  • Spring annotations are controlling object validity instead of the Java type design.
  • These dependencies should probably be constructor parameters.
  • The dependencies should be declared final.
  • Explicit @Autowired fields are unnecessary for this design.

The reviewer should not request Constructor Injection only because of a coding-style rule.

The important architectural reason is that the class should clearly declare everything it requires to function.

9. Reviewer Comment Example

A practical PR review comment could be:

Could we inject these required dependencies through the constructor and make the fields final? This makes the dependencies explicit, prevents reassignment, and allows the service to be unit-tested without relying on Spring field injection.

Another appropriate comment:

OrderRepository, PaymentClient, and NotificationService appear to be mandatory collaborators. Please consider Constructor Injection so that OrderService cannot be created without them.

10. Improved Code

JAVA
@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentClient paymentClient;
    private final NotificationService notificationService;
    public OrderService(
            OrderRepository orderRepository,
            PaymentClient paymentClient,
            NotificationService notificationService) {
        this.orderRepository = orderRepository;
        this.paymentClient = paymentClient;
        this.notificationService = notificationService;
    }
    public OrderResponse placeOrder(OrderRequest request) {
        Order order = new Order();
        order.setCustomerId(request.getCustomerId());
        order.setAmount(request.getAmount());
        Order savedOrder = orderRepository.save(order);
        PaymentResult paymentResult = paymentClient.processPayment(
                savedOrder.getId(),
                savedOrder.getAmount());
        if (paymentResult.isSuccessful()) {
            notificationService.sendOrderConfirmation(savedOrder.getId());
        }
        return new OrderResponse(
                savedOrder.getId(),
                paymentResult.isSuccessful());
    }
}

Because the class contains only one constructor, modern Spring versions can use it for dependency injection without explicit @Autowired.

11. Improved Code Explanation

Dependencies Are Explicit

The constructor clearly shows that OrderService requires:

  • OrderRepository
  • PaymentClient
  • NotificationService

There are no hidden framework-injected dependencies.

Fields Are Final

JAVA
private final OrderRepository orderRepository;

The dependency reference cannot accidentally be reassigned after object construction.

Easier Unit Testing

The service can be instantiated directly.

JAVA
OrderService service = new OrderService(
        orderRepository,
        paymentClient,
        notificationService);

No Spring container is required for an ordinary unit test.

Invalid Construction Is Harder

This is no longer possible:

JAVA
new OrderService();

The compiler requires all constructor arguments.

Reduced Framework Dependency

Spring still manages the service in production, but the Java class itself does not depend on field injection to become usable.

Better Code Review Visibility

The constructor acts as a concise list of collaborators.

If the list becomes very large, reviewers immediately notice that the class may be doing too much.

12. Bad Code vs Improved Code

AreaField InjectionConstructor Injection
Dependency visibilityDependencies are hidden inside fieldsDependencies are visible in constructor
ImmutabilityFields usually remain mutableFields can be final
Unit testingOften requires framework support or reflectionDirect object creation is simple
Object validityObject can exist before fields are injectedRequired dependencies are supplied at construction
Framework couplingStronger dependency on injection mechanismClass remains easier to use as plain Java
Code reviewDependency count is less obviousDependency graph is immediately visible
RefactoringHidden dependencies can be missedConstructor changes expose dependency changes

Constructor Injection usually does not materially change runtime performance.

Its main advantages concern design quality, maintainability, correctness, and testability.

13. Real Project Scenario

Consider an e-commerce checkout microservice.

A CheckoutService depends on:

  • CartRepository
  • InventoryClient
  • PaymentGateway
  • OrderRepository
  • NotificationPublisher

Initially, developers use field injection.

JAVA
@Autowired
private PaymentGateway paymentGateway;

Later, another team creates CheckoutService directly in a unit test.

The object is successfully instantiated, but paymentGateway remains null.

The test fails with a NullPointerException several calls later.

The immediate error appears to be inside payment processing, but the actual design problem occurred when the object was created without its required dependencies.

With Constructor Injection:

JAVA
CheckoutService checkoutService = new CheckoutService(
        cartRepository,
        inventoryClient,
        paymentGateway,
        orderRepository,
        notificationPublisher);

The compiler forces the developer to provide every required collaborator.

This makes object construction safer and tests clearer.

14. Production Impact

Using field injection does not automatically create production failures.

However, weak dependency design can contribute to several problems.

Difficult Maintenance

Hidden dependencies make services harder to understand and refactor.

Runtime Null Failures Outside Spring

If a class is instantiated manually somewhere in production code, injected fields may remain null.

Poor Test Coverage

When classes are difficult to instantiate independently, teams may rely excessively on expensive Spring integration tests rather than focused unit tests.

Architectural Complexity

Because field injection makes adding another dependency very easy, oversized services can grow without the warning signal of an increasingly large constructor.

Difficult Debugging

A failure caused by incomplete object initialization may appear far from the location where the object was created.

Constructor Injection moves these problems closer to object creation.

15. Common Developer Mistakes

Using Field Injection Everywhere

Example:

JAVA
@Autowired
private CustomerRepository customerRepository;

This is convenient but hides required dependencies.

Adding @Autowired to a Single Constructor Without Need

Example:

JAVA
@Autowired
public CustomerService(CustomerRepository customerRepository) {
    this.customerRepository = customerRepository;
}

This is generally unnecessary for a Spring component with one constructor.

Creating Dependencies Manually

Bad:

JAVA
private final PaymentClient paymentClient = new PaymentClient();

This bypasses dependency management and makes testing harder.

Injecting Concrete Classes Unnecessarily

Example:

JAVA
private final StripePaymentGateway stripePaymentGateway;

If the business service logically depends on the PaymentGateway abstraction, depending directly on one implementation reduces flexibility.

Constructor with Too Many Dependencies

Example:

JAVA
public OrderService(
        OrderRepository repository,
        PaymentClient paymentClient,
        InventoryClient inventoryClient,
        CustomerService customerService,
        DiscountService discountService,
        TaxService taxService,
        NotificationService notificationService,
        AuditService auditService,
        MetricsService metricsService,
        FraudService fraudService) {
}

The problem is not Constructor Injection.

The constructor has exposed a deeper design problem: OrderService may have too many responsibilities.

Performing Work Inside the Constructor

Bad:

JAVA
public ProductService(ProductRepository repository) {
    this.repository = repository;
    this.products = repository.findAll();
}

Constructors should normally establish object state rather than trigger database operations.

Accepting Null Dependencies

Bad:

JAVA
public PaymentService(PaymentGateway paymentGateway) {
    this.paymentGateway = paymentGateway;
}

followed later by code that assumes paymentGateway is non-null.

For non-Spring plain Java objects where null can realistically be passed, explicit validation may be appropriate.

Example:

JAVA
this.paymentGateway = Objects.requireNonNull(
        paymentGateway,
        "paymentGateway must not be null");

16. Edge Cases

Null Dependency

When constructing objects manually, null may be passed.

Example:

JAVA
new PaymentService(null);

For strict domain or library code, fail immediately if null dependencies are not valid.

JAVA
public PaymentService(PaymentGateway paymentGateway) {
    this.paymentGateway = Objects.requireNonNull(
            paymentGateway,
            "paymentGateway must not be null");
}

In a correctly configured Spring application, required bean resolution normally fails during startup rather than injecting null.

Multiple Implementations

Suppose two beans implement:

JAVA
public interface NotificationSender {
    void send(String message);
}

with:

JAVA
@Component
public class EmailNotificationSender implements NotificationSender {
}

and:

JAVA
@Component
public class SmsNotificationSender implements NotificationSender {
}

Injecting:

JAVA
public NotificationService(NotificationSender sender) {
    this.sender = sender;
}

can become ambiguous.

A qualifier or primary bean strategy may be required.

Example:

JAVA
public NotificationService(
        @Qualifier("emailNotificationSender") NotificationSender sender) {
    this.sender = sender;
}

Optional Dependency

Do not make a required constructor dependency optional merely to avoid configuration errors.

If functionality is genuinely optional, model that intentionally using an appropriate Spring mechanism or separate component design.

Circular Dependencies

Example:

JAVA
OrderService -> PaymentService -> OrderService

Constructor Injection may expose circular dependencies during startup.

That is useful because circular service dependencies often indicate excessive coupling.

The preferred solution is normally redesigning responsibilities rather than switching back to field injection simply to hide the cycle.

Large Dependency Count

A constructor with many dependencies is an architectural signal.

Review whether the class:

  • Handles multiple workflows
  • Mixes orchestration and business rules
  • Performs unrelated integrations
  • Violates Single Responsibility Principle

17. Performance Considerations

Constructor Injection normally has no meaningful runtime performance disadvantage compared with field injection in ordinary Spring Boot business applications.

Dependency wiring happens primarily during object creation and application startup.

Runtime Complexity

Accessing an injected constructor field is effectively the same as accessing another object field.

There is no meaningful algorithmic complexity difference.

Startup Cost

The difference between constructor and field assignment is negligible for normal application code.

Real Performance Concern

The more important concern is what developers execute inside constructors.

Avoid:

  • Database queries
  • Network calls
  • File processing
  • Large collection loading
  • Expensive calculations

Example of problematic constructor behavior:

JAVA
public CustomerCache(CustomerRepository repository) {
    this.customers = repository.findAll();
}

This can make bean creation slow and cause startup failures.

Constructor Injection itself is not the performance problem.

Doing expensive work during construction is.

18. Security Considerations

Constructor Injection is primarily a design and maintainability technique rather than a direct security mechanism.

However, it can indirectly improve security architecture.

Explicit Security Dependencies

Consider:

JAVA
@Service
public class AccountService {
    private final AuthorizationService authorizationService;
    private final AccountRepository accountRepository;
    public AccountService(
            AuthorizationService authorizationService,
            AccountRepository accountRepository) {
        this.authorizationService = authorizationService;
        this.accountRepository = accountRepository;
    }
}

The authorization dependency is visible and clearly required.

Easier Security Testing

A test can inject mocked authorization behavior and verify denied scenarios.

Avoid Injecting Secrets as Plain Strings

Do not scatter sensitive credentials through constructors such as:

JAVA
public PaymentClient(String apiKey) {
}

without appropriate configuration management.

Prefer strongly defined configuration objects managed through secure configuration mechanisms.

Important Limitation

Constructor Injection does not protect against:

  • SQL injection
  • Authentication bypass
  • Authorization bugs
  • Sensitive logging
  • Data exposure

Those concerns require separate security controls.

19. Testing Considerations

Constructor Injection significantly improves testability.

Consider:

JAVA
public class PaymentService {
    private final PaymentRepository paymentRepository;
    private final PaymentGateway paymentGateway;
    public PaymentService(
            PaymentRepository paymentRepository,
            PaymentGateway paymentGateway) {
        this.paymentRepository = paymentRepository;
        this.paymentGateway = paymentGateway;
    }
    public PaymentResult process(PaymentRequest request) {
        PaymentResult result = paymentGateway.charge(request);
        paymentRepository.save(result);
        return result;
    }
}

Positive Unit Test

Verify successful payment processing.

JAVA
@ExtendWith(MockitoExtension.class)
class PaymentServiceTest {
    @Mock
    private PaymentRepository paymentRepository;
    @Mock
    private PaymentGateway paymentGateway;
    private PaymentService paymentService;
    @BeforeEach
    void setUp() {
        paymentService = new PaymentService(
                paymentRepository,
                paymentGateway);
    }
    @Test
    void shouldSaveSuccessfulPayment() {
        PaymentRequest request = new PaymentRequest(1000);
        PaymentResult result = new PaymentResult(true);
        when(paymentGateway.charge(request)).thenReturn(result);
        PaymentResult actual = paymentService.process(request);
        assertTrue(actual.isSuccessful());
        verify(paymentRepository).save(result);
    }
}

Negative Test

Simulate gateway failure or exception.

JAVA
@Test
void shouldPropagateGatewayFailure() {
    PaymentRequest request = new PaymentRequest(1000);
    when(paymentGateway.charge(request))
            .thenThrow(new PaymentGatewayException("Gateway unavailable"));
    assertThrows(
            PaymentGatewayException.class,
            () -> paymentService.process(request));
    verifyNoInteractions(paymentRepository);
}

Integration Testing

Use Spring integration tests when you need to verify:

  • Bean discovery
  • Multiple implementations
  • @Qualifier
  • @Primary
  • Configuration-based dependencies
  • Complete dependency graph creation

Do not start the complete Spring context merely to test ordinary business logic that can be tested using constructor-created objects.

20. Refactoring Guidelines

When converting existing field-injected code to Constructor Injection, avoid changing unrelated business behavior in the same refactoring.

Suppose the existing class contains:

JAVA
@Service
public class InventoryService {
    @Autowired
    private InventoryRepository inventoryRepository;
    @Autowired
    private WarehouseClient warehouseClient;
    public InventoryResult reserve(InventoryRequest request) {
        return warehouseClient.reserve(request);
    }
}

Step 1: Identify Required Dependencies

Both collaborators appear required.

Step 2: Make Fields Final

JAVA
private final InventoryRepository inventoryRepository;
private final WarehouseClient warehouseClient;

Step 3: Add Constructor

JAVA
public InventoryService(
        InventoryRepository inventoryRepository,
        WarehouseClient warehouseClient) {
    this.inventoryRepository = inventoryRepository;
    this.warehouseClient = warehouseClient;
}

Step 4: Remove Field Injection

Remove @Autowired from fields.

Step 5: Run Unit Tests

Verify existing behavior has not changed.

Step 6: Run Application Context Tests

Confirm Spring resolves all constructor dependencies successfully.

Step 7: Investigate Circular Dependencies

If application startup now reveals dependency cycles, investigate the design instead of automatically changing back to field injection.

Step 8: Keep Refactoring Focused

Do not simultaneously rewrite unrelated business logic unless necessary.

This makes code review safer and reduces regression risk.

21. Best Practices

Use Constructor Injection for Required Collaborators

Repositories, clients, services, validators, and mappers required for normal behavior should generally be constructor dependencies.

Use Final Fields

Example:

JAVA
private final CustomerRepository customerRepository;

This communicates that the dependency is fixed after construction.

Keep Constructors Simple

A constructor should normally:

  • Receive dependencies
  • Validate them when appropriate
  • Assign fields

Avoid performing business processing.

Depend on the Correct Abstraction

If several implementations may exist, inject the interface that represents the business capability.

Treat Large Constructors as Design Feedback

A large constructor may indicate that the service needs decomposition.

Do not hide the problem by changing injection style.

Keep Required and Optional Dependencies Clear

If a dependency is essential, do not model it as optional.

Make Unit Tests Instantiate the Class Directly

For ordinary service logic:

JAVA
new OrderService(repository, paymentClient);

is preferable to loading Spring purely for dependency injection.

22. Practices to Avoid

Field Injection

JAVA
@Autowired
private UserRepository userRepository;

Why avoid it:

  • Hidden dependency
  • Mutable field
  • Harder isolated testing
  • Invalid manual construction possible

Static Service Locator

JAVA
PaymentService paymentService =
        ApplicationContextProvider.getBean(PaymentService.class);

Why avoid it:

  • Dependencies become invisible
  • Strong framework coupling
  • Harder testing
  • Global access

Manual Dependency Construction

JAVA
private final PaymentClient paymentClient = new PaymentClient();

Why avoid it:

  • Tight coupling
  • Difficult mocking
  • Configuration may be bypassed
  • Lifecycle management may be bypassed

Setter Injection for Mandatory Dependencies

JAVA
public void setOrderRepository(OrderRepository orderRepository) {
    this.orderRepository = orderRepository;
}

If the dependency is required, the object can temporarily exist without it.

Huge Constructors Without Investigation

Do not "solve" a constructor with twelve dependencies by moving them back to fields.

The dependency count is exposing a potential responsibility problem.

Heavy Constructor Logic

Avoid:

JAVA
public ProductService(ProductRepository repository) {
    this.products = repository.findAll();
}

Database access during construction can make application startup fragile.

23. Code Review Checklist

When reviewing Constructor Injection, ask:

  • Are all mandatory dependencies passed through the constructor?
  • Are injected dependency fields declared final where appropriate?
  • Is field injection being used unnecessarily?
  • Can this class be instantiated safely outside the Spring container?
  • Does the constructor clearly expose all collaborators used by the class?
  • Is @Autowired unnecessary because the class has only one constructor?
  • Is the class manually constructing a dependency that should be injected?
  • Does the class depend on an implementation where an abstraction would be more appropriate?
  • Are multiple implementations creating bean-resolution ambiguity?
  • Is @Qualifier being used only where necessary?
  • Does the constructor contain database or external API calls?
  • Does the constructor perform business logic?
  • Does the constructor have too many dependencies?
  • Is the large dependency count indicating multiple responsibilities?
  • Are any dependencies nullable even though they are mandatory?
  • Can unit tests create this class without loading the Spring context?
  • Is there a circular dependency between services?
  • Is a circular dependency being hidden instead of redesigned?
  • Are configuration values represented clearly rather than injected as unrelated primitive values?
  • Will this refactoring preserve existing business behavior?

24. Common Pull Request Review Comments

  1. OrderRepository looks like a mandatory dependency. Could we inject it through the constructor and make the field final?
  1. We can remove @Autowired here because this Spring component has a single constructor.
  1. Please avoid creating PaymentClient with new inside this service. Inject the client so its configuration and lifecycle remain managed externally.
  1. This service now has nine constructor dependencies. Could we review whether it has accumulated more than one responsibility?
  1. NotificationSender has multiple implementations. Please make the intended bean selection explicit rather than relying on ambiguous injection.
  1. This repository call is being executed from the constructor. Could we move runtime data loading outside object construction?
  1. The service currently depends directly on StripePaymentGateway. Would depending on the PaymentGateway abstraction better represent what this service actually needs?
  1. This dependency appears mandatory, so setter injection allows an invalid intermediate state. Constructor Injection would make that requirement explicit.
  1. Could we keep these injected fields final? They should not need to change after the service is constructed.
  1. Please avoid resolving this service through the application context from inside business code. Passing it as a constructor dependency would make the relationship explicit and easier to test.

25. Code Review Exercise

Review the following service as if it appeared in a Pull Request.

Identify:

  • Dependency design problems
  • Code smells
  • Maintainability risks
  • Testing difficulties
  • Production risks
  • Appropriate improvements

Do not focus on formatting.

JAVA
@Service
public class RefundService {
    @Autowired
    private RefundRepository refundRepository;
    @Autowired
    private PaymentGateway paymentGateway;
    private NotificationService notificationService;
    private final AuditService auditService =
            new AuditService();
    @Autowired
    public void setNotificationService(
            NotificationService notificationService) {
        this.notificationService = notificationService;
    }
    public RefundService() {
        System.out.println("RefundService created");
    }
    public RefundResult refund(
            Long paymentId,
            BigDecimal amount) {
        Payment payment =
                refundRepository.findPayment(paymentId);
        RefundResult result =
                paymentGateway.refund(payment.getExternalId(), amount);
        auditService.recordRefund(paymentId, amount);
        if (result.isSuccessful()) {
            notificationService.sendRefundNotification(
                    payment.getCustomerId());
        }
        return result;
    }
}

Learner Task

Review the code and determine:

  • Which dependencies should be constructor-injected?
  • Which fields should be final?
  • Why is new AuditService() questionable?
  • Why is setter injection unnecessary here?
  • What testing problems exist?
  • What object-validity problems exist?
  • What should the improved constructor look like?

26. Exercise Solution

The service contains four dependencies:

  • RefundRepository
  • PaymentGateway
  • NotificationService
  • AuditService

All appear necessary for the refund workflow.

Issue 1: Field Injection

JAVA
@Autowired
private RefundRepository refundRepository;

and:

JAVA
@Autowired
private PaymentGateway paymentGateway;

hide required dependencies.

Issue 2: Setter Injection for a Required Dependency

JAVA
@Autowired
public void setNotificationService(...) {
}

If notifications are part of the normal workflow, this dependency should be established when the object is created.

Issue 3: Manual Dependency Creation

JAVA
private final AuditService auditService =
        new AuditService();

RefundService is directly responsible for constructing another service.

This can:

  • Bypass Spring configuration
  • Bypass decorators or proxies
  • Make mocking difficult
  • Create tight coupling

Issue 4: Public No-Argument Constructor

JAVA
public RefundService() {
}

This allows the object to be created without its required dependencies.

Issue 5: Mutable References

Several dependencies cannot be declared final.

Issue 6: Difficult Unit Testing

Testing requires injecting private fields, invoking setters, or starting Spring.

Improved Code

JAVA
@Service
public class RefundService {
    private final RefundRepository refundRepository;
    private final PaymentGateway paymentGateway;
    private final NotificationService notificationService;
    private final AuditService auditService;
    public RefundService(
            RefundRepository refundRepository,
            PaymentGateway paymentGateway,
            NotificationService notificationService,
            AuditService auditService) {
        this.refundRepository = refundRepository;
        this.paymentGateway = paymentGateway;
        this.notificationService = notificationService;
        this.auditService = auditService;
    }
    public RefundResult refund(
            Long paymentId,
            BigDecimal amount) {
        Payment payment =
                refundRepository.findPayment(paymentId);
        RefundResult result =
                paymentGateway.refund(
                        payment.getExternalId(),
                        amount);
        auditService.recordRefund(paymentId, amount);
        if (result.isSuccessful()) {
            notificationService.sendRefundNotification(
                    payment.getCustomerId());
        }
        return result;
    }
}

Why This Is Better

All dependencies are now visible in one place.

The fields are immutable references.

The service cannot normally be created without required collaborators.

Unit tests can easily construct it:

JAVA
RefundService refundService = new RefundService(
        refundRepository,
        paymentGateway,
        notificationService,
        auditService);

No reflection or Spring context is required.

Spring remains responsible for constructing infrastructure and service beans.

27. Interview Perspective

Constructor Injection frequently appears in:

  • Java interviews
  • Spring Boot interviews
  • Senior developer interviews
  • Architecture discussions
  • Pull Request exercises
  • SOLID principle discussions

Interviewers are usually more interested in the reasoning than the syntax.

A weak answer is:

Constructor Injection is better because Spring recommends it.

A stronger answer explains:

  • Required dependencies become explicit.
  • Dependencies can be final.
  • Objects can be instantiated directly in unit tests.
  • Invalid partially initialized objects are harder to create.
  • Dependency count becomes visible.
  • Circular dependencies are exposed.
  • Business classes become less dependent on reflection-based field injection.
  • A large constructor can reveal responsibility problems.

Scenario-Based Discussion

An interviewer may ask:

Your Spring service contains eight @Autowired fields. What would you change?

A strong response would be:

  1. Convert required collaborators to constructor dependencies.
  2. Declare them final.
  3. Check whether all eight are genuinely required.
  4. Review whether the class has too many responsibilities.
  5. Avoid mechanically wrapping dependencies into another object merely to make the constructor shorter.
  6. Write isolated tests using mocks.

28. Interview Questions and Answers

Basic Question

Question: What is Constructor Injection in Spring?

Answer:

Constructor Injection means providing a class's dependencies through its constructor.

Example:

JAVA
@Service
public class UserService {
    private final UserRepository userRepository;
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

Spring creates UserRepository and supplies it when constructing UserService.

It makes required dependencies explicit and allows the field to remain final.

Intermediate Question

Question: Why is Constructor Injection generally preferred over field injection?

Answer:

Constructor Injection provides several practical advantages:

  • Dependencies are explicit.
  • Required dependencies are available when the object is created.
  • Dependency fields can be final.
  • Unit tests can instantiate the class directly.
  • The class relies less on reflection-based field injection.
  • Large dependency counts become visible during code review.

Field injection may be shorter syntactically, but it hides dependency requirements.

Advanced Question

Question: A service has twelve constructor parameters after you replace field injection. Is Constructor Injection causing a design problem?

Answer:

No.

Constructor Injection is exposing an existing design problem rather than creating it.

Twelve dependencies may indicate that the service:

  • Has multiple responsibilities
  • Coordinates too many unrelated systems
  • Contains business logic belonging elsewhere
  • Has become an oversized application service

The correct response is to review the class design.

Moving the dependencies back to fields only hides the coupling.

However, a high dependency count is a design signal rather than automatic proof that the class must be split. The actual responsibilities should be evaluated before refactoring.

Scenario-Based Question

Question: You convert a field-injected Spring service to Constructor Injection and the application reports a circular dependency. What should you do?

Answer:

Investigate the architecture.

For example:

JAVA
OrderService -> PaymentService -> OrderService

may indicate that both services contain responsibilities that are too tightly coupled.

Possible solutions include:

  • Moving shared behavior into another component
  • Changing the direction of orchestration
  • Publishing an event where loose coupling is appropriate
  • Reconsidering service boundaries

Switching back to field injection simply to avoid seeing the circular relationship is usually not the correct architectural fix.

Code-Review Question

Question: What would you comment on this code?

JAVA
@Service
public class ReportService {
    @Autowired
    private ReportRepository reportRepository;
}

Answer:

A suitable review comment would be:

ReportRepository appears to be a required collaborator. Could we inject it through the constructor and keep the field final? That makes the dependency explicit and allows this service to be instantiated directly in unit tests.

Improved version:

JAVA
@Service
public class ReportService {
    private final ReportRepository reportRepository;
    public ReportService(ReportRepository reportRepository) {
        this.reportRepository = reportRepository;
    }
}

Real-Project Question

Question: What practical benefit have you seen from Constructor Injection in large Spring Boot applications?

Answer:

One important benefit is architectural visibility.

When a service constructor starts requiring many repositories, clients, and other services, the dependency list immediately becomes visible during code review.

That often triggers useful discussions about:

  • Service responsibilities
  • Coupling
  • Orchestration boundaries
  • Test complexity
  • Component ownership

Constructor Injection also makes ordinary unit testing easier because the service can be instantiated directly with mocks without loading the Spring application context.

29. Quick Rule to Remember

If a class cannot work correctly without a dependency, require that dependency through the constructor.

30. Final Takeaway

Constructor Injection makes dependency requirements part of the class design rather than hidden framework configuration.

A Java developer should remember:

  • Required collaborators belong in the constructor.
  • Dependency fields should normally be final.
  • Spring does not require @Autowired on a single constructor.
  • Services should not manually construct infrastructure dependencies.
  • Constructors should assign dependencies, not execute business operations.
  • Large constructors should trigger a responsibility review.

During Pull Request review, check whether:

  • Dependencies are explicit.
  • Required dependencies can never be forgotten during normal object construction.
  • Dependencies are immutable references where appropriate.
  • The class can be tested without starting Spring.
  • The class is manually creating objects that should be injected.
  • The constructor has grown large enough to indicate excessive responsibility.
  • Circular dependencies reveal architectural coupling.

Avoid production designs where dependency relationships are hidden through:

  • Field injection
  • Service locators
  • Static access
  • Manual construction of managed services
  • Setter injection for mandatory collaborators

Constructor Injection does not automatically make a class well designed, but it makes the design much easier to see, review, test, and improve.