Spring Boot’s @Autowired vs Constructor Injection: A Detailed Guide

Marcelo Domingues
devdomain
Published in
11 min readSep 5, 2024

--

When working with Spring Boot, one of the most powerful features at your disposal is dependency injection (DI). DI is a design pattern that allows developers to build loosely coupled, easily testable, and maintainable applications by injecting the dependencies a class needs rather than having the class instantiate them itself. In Spring, DI can be achieved using several methods, with @Autowired and constructor injection being two of the most commonly used techniques.

In this article, we will explore @Autowired and constructor injection in-depth, comparing them in various scenarios, discussing best practices, and highlighting the benefits and drawbacks of each approach. By the end of this guide, you will have a comprehensive understanding of these two methods and know how to effectively use them in your Spring Boot applications.

Reference Image

Understanding Dependency Injection in Spring

Before diving into the specifics of @Autowired and constructor injection, it’s essential to have a solid grasp of what dependency injection is and why it’s important in software development.

What is Dependency Injection?

Dependency injection is a technique in which an object’s dependencies are provided (injected) by an external entity rather than the object creating them itself. This external entity is typically a framework, such as Spring, that manages the lifecycle of objects (beans) and injects their dependencies at runtime.

  • Decoupling: DI promotes loose coupling between components in an application. Instead of hard-coding dependencies within a class, you allow the framework to manage them. This decoupling makes it easier to modify, replace, or extend components without affecting other parts of the application.
  • Testability: DI improves the testability of your code. When dependencies are injected, you can easily mock them during testing, leading to more isolated and effective unit tests.
  • Maintainability: DI contributes to more maintainable code. By clearly defining dependencies, it becomes easier to understand how different components interact, reducing the likelihood of introducing bugs when making changes.

Spring provides several ways to achieve dependency injection, including:

  1. Field Injection: Using @Autowired directly on fields.
  2. Setter Injection: Using @Autowired on setter methods.
  3. Constructor Injection: Passing dependencies through the constructor.

Each method has its own advantages and use cases, and understanding when to use each one is crucial for writing clean and effective Spring Boot applications.

The Role of @Autowired in Dependency Injection

@Autowired is a Spring-specific annotation used for automatic dependency injection. It can be applied to fields, setter methods, and constructors, allowing Spring to resolve and inject the necessary dependencies when creating beans.

Field Injection with @Autowired

Field injection is perhaps the simplest and most straightforward way to inject dependencies. By annotating a field with @Autowired, Spring will automatically inject the required bean when the containing bean is created.

Example of Field Injection:

package com.medium;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

@Autowired
private UserRepository userRepository;

public void createUser(User user) {
userRepository.save(user);
}
}

In this example, the UserRepository dependency is injected into the UserService class using @Autowired on the field.

Advantages of Field Injection

  • Simplicity: Field injection is easy to use and involves minimal boilerplate code. You don’t need to write constructors or setters just to inject dependencies.
  • Readability: The dependencies are clearly visible at the top of the class, making it easy to see what the class relies on.

Drawbacks of Field Injection

  • Testability: Field injection can make unit testing more challenging. Since the dependencies are private fields, you need to use reflection to set these fields in your test cases, which can be cumbersome and error-prone.
  • Immutability Issues: With field injection, the injected fields remain mutable, which can lead to unintended side effects if the fields are modified after the object has been constructed.
  • Hidden Dependencies: Dependencies injected via fields are not immediately obvious to someone reading the class, which can make the code harder to understand and maintain. The dependency injection happens behind the scenes, which might obscure the flow of the code.
  • Circular Dependencies: Field injection can mask circular dependencies (where two or more beans depend on each other). While Spring can resolve circular dependencies with field injection, this often indicates a design problem that should be addressed rather than relying on the framework to manage it.

Constructor Injection

Constructor injection is an alternative approach to dependency injection, where dependencies are passed through a class’s constructor. This method is generally preferred over field injection because it offers several significant advantages, particularly in terms of immutability, testability, and ensuring that dependencies are properly set.

Example of Constructor Injection:

package com.medium;

import org.springframework.stereotype.Service;

@Service
public class UserService {

private final UserRepository userRepository;

// Constructor Injection
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}

public void createUser(User user) {
userRepository.save(user);
}
}

In this example, the UserRepository dependency is injected via the constructor, and the userRepository field is marked as final, indicating that it cannot be changed after the object is constructed.

Advantages of Constructor Injection

  • Immutability: Constructor injection allows you to declare dependencies as final, ensuring that they cannot be changed after the object is constructed. This promotes immutability, which is a key principle in building reliable and predictable software.
  • Mandatory Dependencies: Constructor injection enforces that all necessary dependencies are provided at the time the object is created. This prevents issues like NullPointerException that can occur if a required dependency is not set.
  • Testability: Constructor injection significantly improves testability. You can easily create instances of a class with mock dependencies without needing to rely on reflection or other workarounds. This makes unit tests cleaner and more straightforward.
  • Clear Dependencies: With constructor injection, all dependencies are explicitly listed in the constructor, making the class’s dependencies clear and easy to understand. This explicitness is beneficial for code maintenance and collaboration, as it’s immediately obvious what a class needs to function.
  • Avoiding Circular Dependencies: Constructor injection makes it harder to create circular dependencies. If two classes depend on each other, trying to inject them via constructors will result in a compilation error, prompting you to reconsider the design.

Drawbacks of Constructor Injection

  • Boilerplate Code: For classes with many dependencies, constructor injection can lead to a lot of boilerplate code. However, this can be mitigated by using tools like Lombok to generate constructors automatically.
  • Long Parameter Lists: If a class has too many dependencies, the constructor parameter list can become long and unwieldy. This might be a sign that the class is doing too much and may need to be refactored into smaller, more focused classes.
  • Handling Optional Dependencies: If some dependencies are optional, constructor injection can become cumbersome. While you can use Optional in constructors, this can make the code less clean and harder to understand.

Simplifying Constructor Injection with Lombok

One of the drawbacks of constructor injection is the potential for boilerplate code, especially in classes with many dependencies. The Lombok library provides a solution to this problem with the @RequiredArgsConstructor annotation, which automatically generates a constructor for all final fields in a class.

Example with @RequiredArgsConstructor

Here’s how you can use Lombok’s @RequiredArgsConstructor to simplify constructor injection:

package com.medium;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserService {

private final UserRepository userRepository;

public void createUser(User user) {
userRepository.save(user);
}
}

Explanation:

  • @RequiredArgsConstructor Annotation: This Lombok annotation generates a constructor with parameters for all final fields in the class. In this example, Lombok automatically generates a constructor for UserService that takes UserRepository as an argument.
  • Reduced Boilerplate: Using @RequiredArgsConstructor eliminates the need to manually write the constructor, reducing boilerplate code and making the class definition cleaner.

Benefits of Using @RequiredArgsConstructor

  • Cleaner Code: Lombok’s @RequiredArgsConstructor reduces boilerplate, making your code more concise and easier to read.
  • Consistency: If you use constructor injection across your project, Lombok helps maintain consistency by automatically generating constructors for all classes with final dependencies.
  • Focus on Business Logic: By automating the generation of constructors, you can focus more on the business logic of your application rather than the repetitive task of writing constructors.

Potential Drawbacks

  • Hidden Magic: While Lombok reduces boilerplate, it can sometimes obscure what’s happening under the hood. Developers unfamiliar with Lombok might find it harder to understand the code, as the constructors are not explicitly written out.
  • Compile-Time Dependency: Lombok requires annotations processing at compile-time, meaning your project is dependent on Lombok being available and configured correctly in your build system.

@Autowired with Constructor Injection

Spring allows you to use @Autowired with constructor injection, but it’s not always necessary. Since Spring 4.3, if a class has only one constructor, you can omit the @Autowired annotation, and Spring will automatically inject the dependencies.

Example with @Autowired:

package com.medium;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

private final UserRepository userRepository;

@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}

public void createUser(User user) {
userRepository.save(user);
}
}

Example without @Autowired:

package com.medium;

import org.springframework.stereotype.Service;

@Service
public class UserService {

private final UserRepository userRepository;

public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}

public void createUser(User user) {
userRepository.save(user);
}
}

In both examples, Spring will inject the UserRepository dependency when creating the UserService bean. The second example is more concise and preferred when using Spring 4.3 or later.

Benefits of Omitting @Autowired with Constructor Injection

  • Cleaner Code: Omitting @Autowired in classes with a single constructor results in cleaner and less cluttered code.
  • Consistency: If you’re following the convention of using constructor injection and you’re on Spring 4.3 or later, omitting @Autowired helps maintain consistency across your codebase.
  • Automatic Dependency Resolution: Spring’s ability to automatically resolve dependencies based on constructor parameters simplifies the process of dependency injection, reducing the need for explicit annotations.

Field Injection vs. Constructor Injection: A Detailed Comparison

Let’s take a closer look at the differences between field injection and constructor injection, examining key aspects like immutability, testability, and code clarity.

1. Immutability

  • Field Injection: Dependencies injected via fields are mutable, meaning they can be changed after the object is created. This can lead to unintended side effects if the state of the dependency is modified elsewhere in the code.
  • Constructor Injection: Dependencies injected via constructors can be marked as final, making them immutable. This ensures that once the object is created, its dependencies cannot be changed, leading to more predictable and reliable behavior.

Example:

  • Field Injection: The userRepository field in UserService can be reassigned after the object is created.
  • Constructor Injection: The userRepository field in UserService is final, preventing reassignment after construction.

2. Testability

  • Field Injection: Testing classes that use field injection can be more challenging because you need to use reflection to inject mock dependencies. This can make tests more complex and less readable.
  • Constructor Injection: Constructor injection makes testing much easier. You can directly instantiate the class with mock dependencies, leading to simpler and more effective unit tests.

Example:

  • Field Injection: To test UserService, you might need to use reflection to inject a mock UserRepository.
  • Constructor Injection: You can easily pass a mock UserRepository when creating an instance of UserService.

3. Code Clarity

  • Field Injection: Dependencies are injected implicitly, which can make it harder to understand what a class depends on. This lack of explicitness can make the code more difficult to maintain, especially in large codebases.
  • Constructor Injection: Dependencies are explicitly listed in the constructor, making it clear what the class relies on. This explicitness enhances code readability and maintainability, making it easier to collaborate with other developers.

Example:

  • Field Injection: It’s not immediately clear which dependencies are injected just by looking at the class.
  • Constructor Injection: The constructor clearly shows all the dependencies required by the class.

4. Required Dependencies

  • Field Injection: It’s possible to instantiate a class without setting its required dependencies, which can lead to runtime errors like NullPointerException.
  • Constructor Injection: The class cannot be instantiated without providing all the required dependencies, reducing the risk of runtime errors.

Example:

  • Field Injection: If UserService is instantiated without injecting UserRepository, it could throw a NullPointerException.
  • Constructor Injection: UserService cannot be instantiated without providing a UserRepository.

5. Circular Dependencies

  • Field Injection: Spring can resolve circular dependencies with field injection, but this often indicates a design problem. Circular dependencies can lead to hidden issues and make the code harder to understand.
  • Constructor Injection: Circular dependencies are more difficult to handle with constructor injection, as they will cause compilation errors. This forces you to reconsider your design and find a better approach to decouple the classes.

Example:

  • Field Injection: Spring might resolve circular dependencies, but this should be avoided as it indicates poor design.
  • Constructor Injection: Circular dependencies cause immediate compilation errors, prompting you to refactor the code.

When to Use Field Injection

Despite its drawbacks, field injection can still be useful in certain scenarios:

  • Prototyping: When you’re quickly prototyping an application or writing a small utility class, field injection can save time and reduce boilerplate code.
  • Legacy Code: In legacy codebases where constructor injection might require significant refactoring, field injection can be a more pragmatic choice.
  • Circular Dependencies: If you’re dealing with a complex system that inherently has circular dependencies (though this should be avoided if possible), field injection might be the easiest way to resolve them.

When to Use Constructor Injection

Constructor injection is generally the recommended approach, especially in the following situations:

  • Building New Applications: When starting a new project, using constructor injection from the outset will help ensure your code is clean, maintainable, and testable.
  • Enforcing Immutability: If you want to enforce immutability in your classes, constructor injection is the best choice, as it allows you to mark dependencies as final.
  • Improving Testability: If you’re writing unit tests and want to easily mock dependencies, constructor injection is the most straightforward method.
  • Explicitly Declaring Dependencies: In large projects where code clarity and maintainability are crucial, constructor injection makes it clear which dependencies are required for each class.

Best Practices for Dependency Injection in Spring Boot

Here are some best practices to follow when working with dependency injection in Spring Boot:

  1. Favor Constructor Injection Over Field Injection: Constructor injection is generally the best approach because it promotes immutability, improves testability, and makes dependencies explicit.
  2. Use @Autowired Judiciously: If you’re using Spring 4.3 or later and your class has only one constructor, you can omit @Autowired. Use it when you have multiple constructors or when setter injection is necessary.
  3. Avoid Field Injection in Production Code: While field injection can be useful in specific scenarios, it’s best to avoid it in production code in favor of constructor injection for the reasons outlined above.
  4. Handle Optional Dependencies with Care: If you have optional dependencies, consider using Optional in your constructor or using setter injection. This ensures that your code handles these cases gracefully without adding unnecessary complexity.
  5. Refactor Circular Dependencies: If you encounter circular dependencies, consider refactoring your code to eliminate them. Circular dependencies often indicate that your classes are too tightly coupled and need to be broken down into smaller, more focused components.
  6. Use Lombok for Reducing Boilerplate Code: If your classes have many dependencies and the constructor parameter list is getting long, consider using Lombok’s @AllArgsConstructor or @RequiredArgsConstructor to automatically generate constructors and reduce boilerplate code.
  7. Document Your Dependencies: When using constructor injection, it’s good practice to document the dependencies in your constructors. This makes it clear to other developers (and to your future self) why each dependency is needed.

Conclusion

In Spring Boot, both @Autowired and constructor injection are valuable tools for managing dependencies. While @Autowired offers simplicity and quick implementation, it can lead to less maintainable and harder-to-test code due to hidden dependencies and mutable fields.

Constructor injection, on the other hand, is generally recommended. It promotes immutability by making dependencies final, enhances testability by allowing easy mocking, and improves code clarity by explicitly listing all dependencies. This approach leads to more predictable, maintainable, and robust applications. Favor constructor injection as your default method for dependency management in Spring Boot, reserving @Autowired for specific cases where it's truly needed.

Explore More on Spring and Java Development:

Enhance your skills with our selection of articles:

  • Java Lambda Expressions: Techniques for Advanced Developers (Jun 19, 2024): Dive deep into advanced usage of lambda expressions in Java. Read More
  • Mastering Spring Security: Roles and Privileges (Jun 19, 2024): Learn the essentials of managing roles and privileges in Spring Security. Read More
  • Publishing Your Java Library to Maven Central: A Step-by-Step Tutorial (Mar 25, 2024): A comprehensive guide to making your Java library accessible to millions of developers worldwide. Read More
  • The Art of Unit Testing: Elevate Your Code with Effective Mocks (Mar 13, 2024): A complete guide to using mocks in unit testing, emphasizing the benefits of component isolation. Read More
  • Spring Beans Mastery (Dec 17, 2023): Unlock advanced application development techniques. Read More
  • JSON to Java Mapping (Dec 17, 2023): Streamline your data processing. Read More

References:

  1. Spring Boot Official Documentation
    Available at: https://spring.io/projects/spring-boot
  2. Baeldung — Inversion of Control and Dependency Injection in Spring
    Available at: https://www.baeldung.com/inversion-control-and-dependency-injection-in-spring
  3. Spring Dependency Injection Documentation
    Available at: https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-dependencies
  4. Field Injection in Spring — Baeldung
    Available at: https://www.baeldung.com/spring-injecting-collections
  5. Constructor Injection in Spring — Baeldung
    Available at: https://www.baeldung.com/constructor-injection-in-spring

--

--

Marcelo Domingues
devdomain

🚀 Senior Software Engineer | Crafting Code & Words | Empowering Tech Enthusiasts ✨ 📲 LinkedIn: https://www.linkedin.com/in/marcelogdomingues/