Skip to content
Chapter 07 · Spring Boot

Dependency Injection: Field, Setter and Constructor ​

Chapter 6 followed a bean through its lifecycle, and step 4 of that lifecycle was inject dependencies. This chapter is about that step:

  1. the problem dependency injection solves;
  2. the three ways to inject: field, setter, constructor, with the pros and cons of each;
  3. the two problems you will meet: circular dependencies and unsatisfied (ambiguous) dependencies.

The examples use the same User and Order classes as chapter 6.

1. The problem: tight coupling ​

java
public class Order {
    public Order() {
        System.out.println("order initialized");
    }
}

public class User {

    Order order = new Order();        // User creates its own dependency

    public User() {
        System.out.println("user initialized");
    }
}

User depends on Order and creates it itself. The two classes are tightly coupled, which causes two problems.

Problem 1: a change in Order breaks User. The business grows, and Order becomes an interface with several kinds of order:

java
public interface Order { }

public class OnlineOrder implements Order { }
public class OfflineOrder implements Order { }

Now new Order() in User no longer compiles: you cannot instantiate an interface. User has to change too, to new OnlineOrder() or new OfflineOrder(), even though nothing about users changed.

Problem 2: it breaks the Dependency Inversion Principle. The "D" in SOLID says: depend on abstractions, not on concrete implementations. Order order = new OnlineOrder(); depends on the concrete OnlineOrder. To follow the principle, User should depend only on the Order interface, and the actual object should come from outside: whoever creates the User hands it an OnlineOrder or an OfflineOrder.

Dependency injection is how Spring does this. A class declares what it needs, and the IoC container supplies it:

java
@Component
public class User {

    @Autowired
    Order order;                      // "I need an Order"; no new, no concrete class
}

@Component
public class Order { }

User no longer creates or chooses its Order. The responsibility moved to an external source, Spring, which makes the class loosely coupled and independent of how its dependencies are created.

As chapter 6 showed, @Autowired makes the container look for a bean of the required type: if one exists, it is injected; if it has not been created yet, it is created first; if none is defined, startup fails.

2. The three ways to inject ​

There are three places to put @Autowired: on a field, on a setter, or on a constructor. In the experiments below, Order is @Lazy, so its "initialized" line appears exactly when Spring creates it for injection:

java
@Lazy
@Component
public class Order {
    public Order() {
        System.out.println("order initialized");
    }
}

Way 1: field injection ​

@Autowired on the field:

java
@Component
public class User {

    @Autowired
    Order order;

    public User() {
        System.out.println("user initialized");
    }
}
text
user initialized
order initialized

How it works. Spring first calls the constructor (new User()), so at that moment order is still null. Then it uses reflection to go over the class's fields, finds @Autowired on order, sees that no Order bean exists yet, creates one, and sets the field.

Advantage: simple, short and easy to read: one look at the fields shows the dependencies.

Disadvantages:

  1. The field cannot be made immutable. @Autowired private final Order order; does not compile, because a final field must be set by the constructor. Writing private final Order order = null; compiles, but reflection ignores final, and Spring overwrites it anyway:

    text
    final field after injection: Order bean

    So final guarantees nothing with field injection.

  2. NullPointerException when the object is created by hand. Spring does not stop anyone from writing new User(). Such a User is not a bean, nobody injects its field, and order stays null. The first order.process() throws a NullPointerException.

  3. Hard to unit test. To test User with a mock Order, you need a way to put the mock in. There is no constructor parameter and no setter, so tests must use reflection (Mockito's @InjectMocks does exactly that).

Way 2: setter injection ​

@Autowired on a setter method:

java
@Component
public class User {

    Order order;

    public User() {
        System.out.println("user initialized");
    }

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

Spring creates the object, then finds the @Autowired method (again by reflection) and calls it with an Order bean.

Advantages:

  1. The dependency can be changed after creation. Anyone can call the setter again with a different Order implementation.
  2. Easier testing: a test calls user.setOrderDependency(mockOrder); no reflection needed.

Disadvantages:

  1. The field cannot be final, since a setter assigns it after construction.
  2. Harder to read: to know which fields Spring fills, you have to look for setter methods with @Autowired, and notice if one is missing.

@Autowired on the constructor:

java
@Component
public class User {

    private final Order order;

    @Autowired
    public User(Order order) {
        this.order = order;
        System.out.println("user initialized");
    }
}
text
order initialized
user initialized

The order of the lines flipped. To call this constructor, Spring needs an Order first, so it creates the Order and only then calls the constructor. The dependency is resolved at the moment the object is created, not afterwards. @Autowired on a constructor applies to all its parameters.

@Autowired is optional with one constructor. Since Spring 4.3, if a class has exactly one constructor, Spring uses it automatically; the class above works without the annotation.

With several constructors, @Autowired is mandatory:

java
@Component
public class User {

    Order order;
    Invoice invoice;

    public User(Order order) { this.order = order; }

    public User(Invoice invoice) { this.invoice = invoice; }
}
text
Failed to instantiate [com.example.demo.User]: No default constructor found

With two constructors and no @Autowired, Spring does not guess between them. It falls back to the default constructor, which does not exist, and startup fails. Mark the one to use:

java
    @Autowired
    public User(Invoice invoice) { this.invoice = invoice; }
text
invoice initialized
User(Invoice)

Only Invoice is created; Order (lazy and not needed) is never touched.

Why constructor injection is recommended:

  1. All mandatory dependencies exist once the object exists. A User can never be in a half-built state without its Order.
  2. Fields can be final, so they are set once and never changed.
  3. Fail fast. With field injection, forgetting @Autowired is silent: the field stays null and you find out with a NullPointerException at run time, on whatever request first uses it. With constructor injection, a single constructor is used automatically, and an ambiguous set of constructors stops the application at startup.
  4. Easy unit tests, with no Spring and no reflection: new User(mockOrder).

The usual objection is that a class with many dependencies gets a huge constructor. That is really a warning sign: a class with 20 dependencies does too much and should be split.

FieldSetterConstructor
final fieldsnonoyes
When injectedafter constructionafter constructionduring construction
Forgot to injectsilent null, NPE latersilent null, NPE laterfails at startup
Unit testingneeds reflectioncall the setterpass to the constructor
Readabilitybestworstgood
Use it?avoidfor optional, changeable dependenciesdefault choice

3. Problem 1: circular dependency ​

java
@Component
public class Order {
    @Autowired
    Invoice invoice;          // Order needs Invoice
}

@Component
public class Invoice {
    @Autowired
    Order order;              // Invoice needs Order
}

To finish Order, Spring needs Invoice; to finish Invoice, it needs Order. Startup fails:

text
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
|  order (field ...Invoice ...Order.invoice)
↑     ↓
|  invoice (field ...Order ...Invoice.order)
└─────┘

Action:
Relying upon circular references is discouraged and they are prohibited by default. Update your application
to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle
automatically by setting spring.main.allow-circular-references to true.

(Since Spring Boot 2.6, circular references are prohibited by default, whichever injection style is used.) Three ways to fix it, best first.

Fix 1: refactor (the real fix) ​

A cycle usually means both classes share some logic. Move that logic into a third class that both depend on; the cycle disappears.

Fix 2: @Lazy on the injection point ​

java
@Component
public class Invoice {
    @Autowired
    @Lazy
    Order order;
}

Note where @Lazy sits: on the injection point, not on the class. It tells Spring: don't resolve this dependency now; inject a placeholder. Spring injects a proxy that creates or fetches the real Order the first time a method is called on it:

text
invoice initialized
order initialized
invoice.order class = Order$$SpringCGLIB$$0

Invoice is created with the proxy, then Order is created normally (it needs the finished Invoice). The order field holds a generated proxy class, not an Order, and the cycle is broken.

Fix 3: wire one direction yourself in @PostConstruct (a hack) ​

Let Spring inject only one direction, and set the other reference yourself once the bean is complete:

java
@Component
public class Order {

    @Autowired
    Invoice invoice;

    @PostConstruct
    public void initialize() {
        invoice.setOrder(this);       // the back-reference, set by our code
    }
}

@Component
public class Invoice {

    Order order;                      // no @Autowired

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

Spring creates Order, then creates Invoice (its order is null, which is fine because nothing is injected there) and injects it into Order. Then @PostConstruct sets Invoice's reference. It works, but only refactoring removes the design problem; fixes 2 and 3 make code work when you cannot change it.

4. Problem 2: unsatisfied dependency (two candidates) ​

Back to Order as an interface with two implementations, both beans:

java
public interface Order { }

@Component
public class OnlineOrder implements Order { }

@Component
public class OfflineOrder implements Order { }

@Component
public class User {
    @Autowired
    Order order;
}

Which object should order get?

text
Field order in com.example.demo.User required a single bean, but 2 were found:
	- onlineOrder: defined in ...OnlineOrder.class
	- offlineOrder: defined in ...OfflineOrder.class

Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier
to identify the bean that should be consumed

Fix 1: @Primary ​

Mark one implementation as the default choice:

java
@Primary
@Component
public class OnlineOrder implements Order { }
text
online order initialized
user got: online
offline order initialized

All three beans are still created (none is lazy), but wherever an Order is needed and nothing more specific is said, the primary one is injected.

Fix 2: @Qualifier ​

Give the beans names and ask for one by name at the injection point:

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

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

@Component
public class User {
    @Autowired
    @Qualifier("offlineOrderName")
    Order order;
}
text
offline order initialized
user got: offline
online order initialized

@Primary sets a default for the whole application; @Qualifier chooses per injection point and takes precedence. (Without @Qualifier on the classes, the default bean names, onlineOrder and offlineOrder, work as qualifiers too.)

The choice is still fixed in code: switching to online orders means editing @Qualifier. Chapter 9 makes it configurable.

Next: Every bean so far existed exactly once, the singleton default. Chapter 8 covers bean scopes: a new instance for every injection (prototype), one per HTTP request, or one per user session, and the trap of injecting a short-lived bean into a singleton.

Check yourself ​

  1. Give two problems with Order order = new OnlineOrder(); inside User.
  2. What does dependency injection change about who creates a dependency?
  3. With field injection, what is the value of the field while the constructor runs, and how does Spring set it afterwards?
  4. Why does final mean nothing with field injection?
  5. When would a hand-made new User() throw a NullPointerException?
  6. Why does order initialized print before user initialized with constructor injection, but after it with field injection?
  7. When is @Autowired optional on a constructor? What happens with two constructors and no @Autowired?
  8. Give three reasons constructor injection is recommended.
  9. Order and Invoice depend on each other. What happens at startup? Give three fixes, best first.
  10. Where must @Lazy go to break a cycle, and what does Spring inject there?
  11. Two beans implement Order. What are the two ways to choose one, and how do they differ?
Answers
  1. Any change to Order (such as making it an interface) forces a change in User (tight coupling); and User depends on a concrete class, breaking the Dependency Inversion Principle.
  2. The class no longer creates or chooses it; it declares what it needs and the IoC container supplies it.
  3. null. After construction, Spring uses reflection to find @Autowired fields and sets them.
  4. Spring sets the field through reflection, which ignores final, so the field is changed after its initialisation.
  5. Spring never injects into objects it did not create, so the field stays null; the first method that uses it throws.
  6. Spring needs the Order to call the constructor, so it creates it first. With field injection, the constructor runs first and the field is filled afterwards.
  7. When the class has exactly one constructor (Spring 4.3+). With two and no @Autowired, Spring looks for a default constructor, finds none, and startup fails ("No default constructor found").
  8. Any three of: dependencies can be final; all mandatory dependencies exist once the object exists; problems fail at startup rather than as a NullPointerException later; easy unit tests by passing mocks to the constructor.
  9. Startup fails: the dependencies form a cycle, which is prohibited by default. Fixes: refactor the shared logic into a third class; @Lazy on one injection point; set one direction yourself in @PostConstruct.
  10. On the injection point (the @Autowired field, setter or constructor parameter), not on the class. Spring injects a proxy that resolves the real bean on first use.
  11. @Primary on one implementation (a default everywhere) or @Qualifier at the injection point (chosen per injection, and it takes precedence over @Primary).