Skip to content

Dependency Injection in Spring Boot | With Advantages and Disadvantages

Real World Analogy

Imagine you are building a mobile app and you need a payment gateway. Instead of building your own payment system from scratch inside your app, you call an external payment provider (Stripe, Razorpay, etc.) and they handle the payments for you. Your app does not care which provider it is; it just calls a standard interface. This is dependency injection: your class declares what it needs, and an external source (Spring) provides the actual implementation.


The Problem Dependency Injection Solves

Before understanding dependency injection, let us look at what happens without it.

Tightly Coupled Code

java
public class Order {
    public void createOrder() {
        System.out.println("Creating order");
    }
}

public class User {
    // User directly creates its own dependency
    private Order order = new Order();

    public void processUserOrder() {
        order.createOrder();
    }
}

This looks fine until requirements change. Now suppose Order needs to become an interface with multiple implementations:

java
public interface Order {
    void createOrder();
}

public class OnlineOrder implements Order {
    public void createOrder() { System.out.println("Online order created"); }
}

public class OfflineOrder implements Order {
    public void createOrder() { System.out.println("Offline order created"); }
}

Now User is broken because you cannot call new Order() on an interface. You are forced to change User every time Order changes. Worse, writing new OnlineOrder() directly inside User violates the Dependency Inversion Principle (the D in SOLID), which says:

High level modules should not depend on low level modules. Both should depend on abstractions. Dependencies should come from outside, not be created inside.

How Dependency Injection Fixes This

Dependency injection makes a class independent of how its dependencies are created. The class declares what it needs; Spring provides the actual implementation.

java
@Component
public class User {

    // Declare dependency on the abstraction (interface), not the implementation
    private final Order order;

    @Autowired
    public User(Order order) {
        // Spring injects whichever Order implementation it finds
        this.order = order;
    }

    public void processUserOrder() {
        order.createOrder();
    }
}

Now User depends on the Order interface, not on OnlineOrder or OfflineOrder. Spring decides at runtime which implementation to inject. The class is loosely coupled.


Three Types of Dependency Injection

Spring supports three injection styles. Understanding their trade offs is essential for interviews and real world code reviews.

1. Field Injection

The dependency is injected directly into the field using @Autowired.

java
@Component
public class User {

    @Autowired
    private Order order;  // Spring injects using reflection

    public void processUserOrder() {
        order.createOrder();
    }
}

How it works internally: Spring uses Java Reflection to scan all fields of the class and inject any field annotated with @Autowired. This happens after the object is constructed.

Advantages

  • Simple and easy to read at a glance
  • Minimal boilerplate code

Disadvantages

1. Cannot make fields immutable (final)

java
@Autowired
private final Order order; // Compiler error — cannot assign to final field

Even if you try to initialise the field with null to work around the compiler error, Spring's reflection will override it at runtime. True immutability is impossible with field injection.

2. Risk of NullPointerException

java
// Some other code manually creates a User
User user = new User(); // Uses default constructor
user.processUserOrder(); // order is null! NullPointerException

When someone creates your class with new User() instead of letting Spring manage it, the @Autowired field is never populated. Spring is not involved in manual object creation.

3. Difficult to unit test

java
// How do you inject a mock Order into User for testing?
User user = new User();
// There is no constructor or setter to pass your mock through
// You must use reflection yourself (Mockito's @InjectMocks does this)

2. Setter Injection

The dependency is injected through a setter method annotated with @Autowired.

java
@Component
public class User {

    private Order order;

    @Autowired
    public void setOrder(Order order) {
        this.order = order;
    }

    public void processUserOrder() {
        order.createOrder();
    }
}

How it works: Spring creates the object using the default constructor, then calls the setter method to inject the dependency.

Advantages

  • Dependency can be changed after object creation: Because it is a setter, you can call it again at any time with a different implementation.
  • Easier to unit test than field injection: You can call the setter directly in your test to inject a mock.
java
// In a test
User user = new User();
user.setOrder(mockOrder); // Clean, no reflection needed
user.processUserOrder();

Disadvantages

  • Cannot make fields immutable: A field with a setter cannot be final.
  • Difficult to read: Someone reading the class must hunt for setter methods with @Autowired to understand what dependencies the class has.
  • Object can be used in a partially initialised state: If the setter is not called before the object is used, the field is null.

The dependency is resolved at the moment the object is created. This is the industry standard approach.

java
@Component
public class User {

    // final — immutable once set
    private final Order order;

    @Autowired // Optional when there is only one constructor (Spring 4.3+)
    public User(Order order) {
        this.order = order;
    }

    public void processUserOrder() {
        order.createOrder();
    }
}

From Spring 4.3 onwards, if a class has exactly one constructor, the @Autowired annotation is optional. Spring automatically uses that constructor for injection.

Advantages

1. All dependencies are guaranteed to exist at construction time The object is never in an invalid state. You cannot create a User without providing an Order.

2. Fields can be made immutable (final)

java
private final Order order; // Immutable — set once, never changed

3. Fail fast: missing dependencies cause startup failure, not runtime NPE If Spring cannot find a bean to inject into the constructor, the application fails at startup with a clear error message — not silently at runtime.

4. Easy to unit test

java
// In a test — no Spring needed, no reflection needed
Order mockOrder = Mockito.mock(Order.class);
User user = new User(mockOrder);
user.processUserOrder();

5. Encourages good design A constructor with many parameters is a visible warning that the class has too many responsibilities. It naturally pushes you to refactor bloated classes.

When @Autowired is mandatory

If a class has more than one constructor, you must annotate exactly one with @Autowired to tell Spring which one to use.

java
@Component
public class User {

    private final Order order;
    private final Invoice invoice;

    // Spring will use this constructor because of @Autowired
    @Autowired
    public User(Order order, Invoice invoice) {
        this.order = order;
        this.invoice = invoice;
    }

    // Second constructor — Spring ignores this
    public User(Order order) {
        this.order = order;
        this.invoice = null;
    }
}

Common Dependency Injection Problems

Problem 1: Circular Dependency

java
@Component
public class Order {
    @Autowired
    private Invoice invoice; // Order depends on Invoice
}

@Component
public class Invoice {
    @Autowired
    private Order order; // Invoice depends on Order — circular!
}

Spring detects this cycle and throws a BeanCurrentlyInCreationException.

Solution 1: Refactor (Best Practice)

Extract the shared logic both classes need into a third class:

java
@Component
public class SharedUtil { /* common logic */ }

@Component
public class Order {
    @Autowired private SharedUtil util;
}

@Component
public class Invoice {
    @Autowired private SharedUtil util;
}

Solution 2: @Lazy on @Autowired

java
@Component
public class Order {
    @Autowired
    @Lazy  // Spring creates a proxy instead of the real object at startup
    private Invoice invoice;
}

With @Lazy on the autowired field, Spring creates a proxy placeholder at bean construction time. The real Invoice object is created only when you actually call a method on it.

Solution 3: @PostConstruct Manual Wiring (Hack, Avoid if Possible)

java
@Component
public class Invoice {
    private Order order; // No @Autowired

    @Autowired
    private ApplicationContext context;

    @PostConstruct
    public void init() {
        this.order = context.getBean(Order.class); // Manually fetch
    }
}

This works but is not recommended. Prefer refactoring.


Problem 2: Unsatisfied Dependency (Multiple Implementations)

java
public interface Order { void createOrder(); }

@Component
public class OnlineOrder implements Order { ... }

@Component
public class OfflineOrder implements Order { ... }

@Component
public class User {
    @Autowired
    private Order order; // Spring does not know which one to inject!
}

Spring sees two beans matching the Order type and throws NoUniqueBeanDefinitionException.

Solution 1: @Primary

Mark one implementation as the default:

java
@Component
@Primary  // Use this one by default
public class OnlineOrder implements Order { ... }

Spring injects OnlineOrder whenever Order is required unless overridden.

Solution 2: @Qualifier

Explicitly name implementations and select by name:

java
@Component
@Qualifier("onlineOrderImpl")
public class OnlineOrder implements Order { ... }

@Component
@Qualifier("offlineOrderImpl")
public class OfflineOrder implements Order { ... }

@Component
public class User {

    @Autowired
    @Qualifier("onlineOrderImpl")  // Be explicit about which one you want
    private Order order;
}

Solution 3: Inject Both and Choose Dynamically (Industry Standard)

java
@Component
public class User {

    @Autowired
    @Qualifier("onlineOrderImpl")
    private Order onlineOrder;

    @Autowired
    @Qualifier("offlineOrderImpl")
    private Order offlineOrder;

    public void processOrder(boolean isOnline) {
        Order selected = isOnline ? onlineOrder : offlineOrder;
        selected.createOrder();
    }
}

This approach respects the Dependency Inversion Principle while still allowing dynamic selection at runtime based on business logic.


Comparison Table

FeatureField InjectionSetter InjectionConstructor Injection
Immutability (final)Not possibleNot possibleSupported
NPE risk on manual newHighMediumNone
Unit test friendlinessPoorAcceptableExcellent
Fail fast on missing depNoNoYes (at startup)
RecommendedNoNoYes

Summary

Dependency injection is the mechanism by which Spring supplies a class with the objects it needs, rather than having the class create them itself. This achieves:

  1. Loose coupling between classes
  2. Adherence to the Dependency Inversion Principle (depend on abstractions)
  3. Testability (you can swap real dependencies with mocks easily)

Always prefer constructor injection for mandatory dependencies. It gives you immutability, fail fast startup, and clean testability.


Interview Questions

Q1. What is Dependency Injection? Why do we need it? Dependency injection is a design pattern where an object's dependencies are provided by an external source (the Spring container) rather than being created by the object itself. It promotes loose coupling, makes code testable, and adheres to the Dependency Inversion Principle of SOLID.

Q2. What are the three types of dependency injection in Spring? Which one is recommended? Field injection, setter injection, and constructor injection. Constructor injection is recommended because it supports immutability, guarantees all dependencies exist at construction time, and enables fail fast behaviour at application startup.

Q3. Why can you not use final with field injection? Field injection uses Java Reflection to inject values after object construction. Although reflection can bypass final at runtime (injecting through the field), the true contract of final (initialise once, never change) is broken. Constructor injection initialises final fields in the constructor properly.

Q4. What is circular dependency? How do you resolve it? Circular dependency occurs when Bean A depends on Bean B and Bean B depends on Bean A. The best solution is refactoring: extract the shared logic into a third bean. If refactoring is not possible, you can use @Lazy on one of the @Autowired fields to break the cycle with a proxy.

Q5. What is @Qualifier? When do you use it?@Qualifier is used when multiple beans of the same type exist and you need to specify which one to inject. You give each implementation a qualifier name and reference that name at the injection point.

Q6. What is the difference between @Primary and @Qualifier?@Primary marks one bean as the default choice when multiple beans of the same type exist. @Qualifier allows you to precisely select a specific bean by name regardless of any primary designation. @Qualifier takes precedence over @Primary.

Q7. What happens if you create a Spring managed class with new instead of letting Spring inject it? Spring is not involved in the creation, so @Autowired fields are never populated. They remain null. Any attempt to use those dependencies at runtime will result in a NullPointerException. This is one of the key disadvantages of field injection.

Q8. From Spring 4.3 onwards, when can you omit @Autowired on a constructor? When a class has exactly one constructor, Spring automatically uses it for dependency injection without needing the @Autowired annotation. If the class has more than one constructor, you must annotate exactly one with @Autowired.