Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 33

IoC & DI: Constructor vs. Setter Injection

Recommended resources

Hello! Welcome to the first lesson in our "Spring & Spring Boot Core" module. In our previous module, we delved deep into advanced Core Java topics like serialization. We're now making a pivotal transition from the foundational principles of the Java language to the powerful frameworks that build upon them. Spring is arguably the most important framework in the Java ecosystem, and understanding its core principles is non-negotiable for any senior developer interview.

Today's learning outcome is to explain Inversion of Control (IoC), Dependency Injection (DI), and compare constructor vs. setter injection. These concepts form the absolute bedrock of the Spring framework. Mastering them will not only help you write better, more maintainable code but also enable you to confidently answer some of the most common and fundamental questions asked in FAANG and Fintech interviews.

1. The Core Principle: Inversion of Control (IoC)

In traditional programming, your code is in complete control. You write the main method, you decide when to create objects (new MyObject()), and you call methods on those objects. Your code makes calls to libraries, but the overall flow is dictated by you.

Inversion of Control (IoC) flips this model on its head.

With IoC, you yield control of the program's flow to a container or framework (like Spring). Instead of your code calling the framework, the framework calls your code at the appropriate time. You write the business logic components, but the framework is responsible for "wiring" them together and managing their lifecycle.

Think of it this way:

  • Traditional: You build a car from scratch, connecting the engine, wheels, and steering yourself. You are in control of assembling all the parts.
  • IoC: You go to a car factory (the framework) and provide specifications for the engine and chassis (your components). The factory assembles the car for you and delivers the final product. The control of assembly has been "inverted" from you to the factory.

This might seem abstract, so let's explore what this principle means in practice.

Inversion of Control and Dependency Injection with Spring

The article 'Inversion of Control and Dependency Injection with Spring' on Baeldung provides an excellent starting point. Let's read the first section to solidify our understanding of IoC.

Read section 2, 'What Is Inversion of Control?'. Focus on the definition and the stated advantages, as these are key talking points for an interview.

As the article mentions, the key benefits of IoC are:

  • Decoupling: Your components don't need to know about the concrete implementations of their dependencies.
  • Modularity: It's easier to swap implementations without changing the core business logic.
  • Testability: You can easily substitute real dependencies with mock objects in your unit tests, which is crucial for building robust systems.

2. A Practical Implementation of IoC: Dependency Injection (DI)

IoC is a broad principle. Dependency Injection (DI) is the most common design pattern used to implement IoC.

With DI, the control being inverted is specifically the process of an object getting its dependencies.

  • Without DI: An object is responsible for creating its own dependencies.

    public class OrderService {
        // The OrderService is tightly coupled to a specific database repository.
        // To change it, you must modify this class.
        private final MySqlOrderRepository repository = new MySqlOrderRepository();
    
        public void processOrder(Order order) {
            // ...
            repository.save(order);
        }
    }
    
  • With DI: An object is given its dependencies from an external source (the "injector"). It does not create them itself.

    public class OrderService {
        // The OrderService depends on an abstraction (interface), not a concrete class.
        private final OrderRepository repository;
    
        // The dependency is "injected" via the constructor.
        public OrderService(OrderRepository repository) {
            this.repository = repository;
        }
    
        public void processOrder(Order order) {
            // ...
            repository.save(order);
        }
    }
    

In this second example, the OrderService is no longer responsible for creating the OrderRepository. An external entity, the Spring IoC Container, will create an instance of a class that implements OrderRepository (e.g., MySqlOrderRepository or PostgresOrderRepository) and "inject" it into the OrderService when OrderService is created.

This relationship is perfectly illustrated in the following diagram.

Inversion of Control (IoC) and Dependency Injection (DI) Overview
This flowchart shows that Inversion of Control (IoC) is a general principle. Dependency Injection (DI) and Dependency Lookup (DL) are two patterns that implement this principle. Spring heavily relies on DI.

To further clarify this fundamental shift from traditional object creation to DI, please read the next sections from the Baeldung article and the official Spring documentation.

Inversion of Control and Dependency Injection with Spring

These sections will walk you through the concept of DI and introduce the Spring IoC Container, which is the 'injector' in the Spring world.

Read section 3 ('What Is Dependency Injection?') and section 4 ('The Spring IoC Container'). Note the 'Store' and 'Item' example, as it clearly contrasts the two approaches.


3. Types of Dependency Injection

Now that we understand what DI is, the next crucial interview topic is how Spring can perform this injection. Spring primarily supports three types of DI. For your interviews, you must know them and, more importantly, know which one to prefer and why.

a) Constructor-Based Dependency Injection

This is the most recommended approach. The container invokes a class constructor, passing the dependencies as arguments.

@Component
public class OrderService {
    private final OrderRepository repository;

    // The @Autowired annotation on the constructor tells Spring to inject 
    // a bean of type OrderRepository here. Since Spring 4.3, if a class
    // has only one constructor, the @Autowired is optional.
    @Autowired
    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }
}

Key Characteristics:

  • Dependencies can be declared as final, ensuring immutability.
  • It guarantees that the object is in a fully initialized state upon creation. It cannot be instantiated without its required dependencies.
  • Clearly exposes the mandatory dependencies of a class. A constructor with many parameters is a "code smell" that signals the class might be violating the Single Responsibility Principle.

b) Setter-Based Dependency Injection

With this approach, the container first calls a no-argument constructor to instantiate the object and then calls setter methods to inject the dependencies.

@Component
public class OrderService {
    private OrderRepository repository;

    public OrderService() {
        // No-arg constructor
    }

    @Autowired
    public void setOrderRepository(OrderRepository repository) {
        this.repository = repository;
    }
}

Key Characteristics:

  • Allows for dependencies to be optional.
  • The object can be re-configured or re-injected after it has been created.
  • Dependencies are not final and the object state is mutable.

c) Field-Based Dependency Injection

Here, dependencies are injected directly into the fields, typically using the @Autowired annotation.

@Component
public class OrderService {
    @Autowired
    private OrderRepository repository;

    // ... business logic
}

While this looks very clean and is common in many codebases, it is generally not recommended. Senior developers are expected to know its significant drawbacks:

  • Hides Dependencies: The class's required dependencies are not visible in its public contract (constructors or methods).
  • Harder to Test: When you want to unit test the class, you can't simply instantiate it with new OrderService(mockRepository). You must use reflection to set the private field, which is cumbersome.
  • Encourages Bad Practice: It's too easy to add many dependencies, which can lead to classes with too many responsibilities.
  • Tight Coupling to the DI Container: The class is difficult to use outside of a DI container context.
Test your understanding!

You are reviewing a colleague's code and see the following class. From an architectural and testability standpoint, what feedback would you give?

@Service
public class ReportGenerator {
    @Autowired

    private CustomerRepository customerRepo;
    @Autowired
    private OrderRepository orderRepo;
    @Autowired
    private ProductRepository productRepo;
    @Autowired
    private PdfExporter pdfExporter;
    @Autowired
    private EmailService emailService;

    // ... methods to generate and email a complex report
}
Show answer

The primary feedback should be to switch from field injection to constructor injection.

  1. Why? Field injection makes the class hard to test in isolation (unit tests) and hides its dependencies. It's tightly coupled to the Spring container.
  2. The "Code Smell": Refactoring to constructor injection would result in a constructor with five arguments: public ReportGenerator(CustomerRepository cr, OrderRepository or, ...). This immediately reveals a design problem: the ReportGenerator class likely has too many responsibilities (data fetching, report creation, exporting, emailing) and should be refactored to better adhere to the Single Responsibility Principle. Field injection hid this design flaw, while constructor injection makes it obvious.

4. Comparison: Constructor vs. Setter Injection

The choice between constructor and setter injection is a classic interview question. The official Spring documentation provides a clear recommendation.

Dependency Injection :: Spring Framework

The Spring Framework's official documentation is the ultimate source of truth. This section provides a definitive answer on when to use each type of injection.

Read the section titled 'Constructor-based or setter-based DI?'. This is a critical piece of reading. Pay close attention to the Spring team's official recommendation and the reasoning behind it.

To summarize the key points for your interviews:

Feature Constructor Injection Setter Injection
Dependency Type Ideal for mandatory dependencies. Suitable for optional dependencies.
Immutability Can declare fields as final, ensuring immutability. Dependencies must be mutable.
Object State Guarantees a fully initialized object upon creation. Object is created first, then dependencies are set.
Circular Dependencies A circular dependency will cause a BeanCurrentlyInCreationException on startup. This fails fast, which is good. Can resolve some circular dependencies, but this often indicates a design problem. We will cover this in a future lesson.
Best Practice Recommended by the Spring team for critical dependencies. Use sparingly for non-critical or configurable dependencies.

The rule of thumb is clear: Use constructor injection for all mandatory dependencies. Use setter injection only for optional dependencies that have a sensible default value.


Conclusion

Congratulations on completing your first lesson on Spring! You've tackled the most fundamental concepts that underpin the entire framework. Understanding the "why" behind IoC and DI, not just the "how" of @Autowired, is what distinguishes a senior developer.

Key Takeaways:

  • Inversion of Control (IoC): A design principle where a framework controls the program flow and calls your custom code, rather than the other way around. This promotes decoupling.
  • Dependency Injection (DI): A specific pattern to achieve IoC, where an object's dependencies are provided ("injected") by an external entity (the IoC container) instead of being created by the object itself.
  • Constructor Injection: The preferred method for mandatory dependencies. It creates immutable, fully initialized objects and is inherently more robust and testable.
  • Setter Injection: A viable option for optional dependencies that may need to be reconfigured.
  • Field Injection: Should be avoided due to its negative impact on testability and its tendency to hide design problems.

Preview of the Next Lesson:
Now that we know how the Spring container creates objects and injects their dependencies, a logical next question is: what happens after that? In our next lesson, we will explore the Spring bean lifecycle, its scopes (like singleton and prototype), and how to hook into this lifecycle with callbacks.