Spring Dependency Injection: Constructor vs Setter vs Field

Introduction

This tutorial explains dependency injection in Spring, and compares its three styles: constructor injection, setter injection and field injection. We will start with what Spring does when it injects a dependency and what @Autowired means. We will then write the same class in each of the three styles and see what each one makes easy or hard, including what happens when the class is used in a unit test without Spring. Next we will look at optional dependencies, injecting all beans of a type as a list or a map, and classes with more than one constructor. Lastly, we will see how each style behaves with circular dependencies, and end with a clear recommendation.

(The idea of dependency injection itself, independent of Spring, is explained in Dependency Injection vs Dependency Inversion vs IoCClick to read: Dependency Injection vs Dependency Inversion vs IoC. This tutorial is about how Spring does it.)

What Spring does when it injects a dependency

A dependency is an object that another object needs to do its work - a service needs a repository, a controller needs a service. When the container creates a bean, it looks at what the bean asks for, finds a matching bean (by default by type), and passes it in. A bean can ask for its dependencies in three places -
  • Constructor parameters: Spring calls the constructor with the matching beans as arguments.
  • Setter methods: Spring creates the bean first, then calls a setter for each dependency.
  • Fields: Spring creates the bean, then writes each dependency straight into a field using reflection.
@Autowired is the annotation that marks a constructor, a setter or a field as an injection point. It is optional on a constructor when the class has only one, which is the most common case.

Constructor injection

With constructor injection, the dependencies are constructor parameters, stored in final fields. The example has a PayrollService that needs a SalaryRepository and a TaxCalculator -
Constructor injection - dependencies are final fields set by the constructor
//ConstructorInjectionApp.java
package com.javabrahman.spring.di.constructor;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;

@Repository
class SalaryRepository {
  double grossSalary(String employee) {
    return 5000.0;
  }
}

@Component
class TaxCalculator {
  double tax(double gross) {
    return gross * 0.2;
  }
}

@Service
class PayrollService {
  private final SalaryRepository salaryRepository;
  private final TaxCalculator taxCalculator;

  // The only constructor, so @Autowired is not needed
  PayrollService(SalaryRepository salaryRepository, TaxCalculator taxCalculator) {
    this.salaryRepository = salaryRepository;
    this.taxCalculator = taxCalculator;
  }

  String payslip(String employee) {
    double gross = salaryRepository.grossSalary(employee);
    return employee + ": gross " + gross + ", net " + (gross - taxCalculator.tax(gross));
  }
}

@Configuration
@ComponentScan
class AppConfig {
}

public class ConstructorInjectionApp {
  public static void main(String[] args) {
    try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
      System.out.println(context.getBean(PayrollService.class).payslip("Tom Jones"));
    }
  }
}
 OUTPUT of the above code
Tom Jones: gross 5000.0, net 4000.0

Explanation of the code

  • PayrollService has a single constructor, so Spring uses it without any annotation, finds the SalaryRepository and TaxCalculator beans by type, and passes them in.
  • The fields are final: once the object exists, its dependencies are set and can never change or be null.
  • The constructor is the complete list of what the class needs. Anyone reading the class, or creating it in a test, sees it at once.
  • The class has no Spring import except the @Service annotation; the constructor is plain Java.

Setter injection

With setter injection, Spring first creates the bean with its no-argument constructor, then calls each setter annotated with @Autowired. Setter injection suits optional dependencies, which can be left out without breaking the bean. With @Autowired(required = false), Spring simply skips the setter when there is no matching bean -
Setter injection, with one required and one optional dependency
//SetterInjectionApp.java
package com.javabrahman.spring.di.setter;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

@Component
class ReportStore {
  void save(String report) {
    System.out.println("Saved: " + report);
  }
}

interface AuditTrail {   // no bean implements this interface in this example
  void record(String event);
}

@Service
class ReportService {
  private ReportStore reportStore;
  private AuditTrail auditTrail;

  @Autowired
  void setReportStore(ReportStore reportStore) {
    this.reportStore = reportStore;
  }

  @Autowired(required = false)   // skipped if there is no AuditTrail bean
  void setAuditTrail(AuditTrail auditTrail) {
    this.auditTrail = auditTrail;
  }

  void generate(String name) {
    reportStore.save(name);
    if (auditTrail != null) {
      auditTrail.record("generated " + name);
    } else {
      System.out.println("No audit trail configured - skipping audit");
    }
  }
}

@Configuration
@ComponentScan
class AppConfig {
}

public class SetterInjectionApp {
  public static void main(String[] args) {
    try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
      context.getBean(ReportService.class).generate("Q3 sales report");
    }
  }
}
 OUTPUT of the above code
Saved: Q3 sales report
No audit trail configured - skipping audit

Explanation of the code

  • Spring created ReportService with its default constructor, then called setReportStore() with the ReportStore bean.
  • No bean implements AuditTrail, and required = false told Spring that this is fine, so setAuditTrail() was never called and the field stayed null.
  • The cost: the fields cannot be final, and between construction and the setter calls the object exists in an incomplete state. The class has to cope with null dependencies.

Field injection

Field injection puts @Autowired directly on the fields. It is the shortest to write, which is why it is common in older code and quick examples -
Field injection - @Autowired directly on the fields
@Service
class PayrollService {
  @Autowired
  private SalaryRepository salaryRepository;

  @Autowired
  private TaxCalculator taxCalculator;

  String payslip(String employee) { ... }
}
Inside a running Spring application it works - Spring writes the beans into the private fields with reflection. The problems show up outside the container. A unit test usually creates the class under test with new and passes in test doubles. The example below does exactly that with a field-injected class and a constructor-injected class -
Creating field-injected and constructor-injected classes without Spring, as a unit test would
//FieldInjectionTestApp.java
package com.javabrahman.spring.di.field;

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

class TaxCalculator {
  double tax(double gross) {
    return gross * 0.2;
  }
}

class FieldInjectedPayroll {
  @Autowired
  private TaxCalculator taxCalculator;

  double net(double gross) {
    return gross - taxCalculator.tax(gross);
  }
}

class ConstructorInjectedPayroll {
  private final TaxCalculator taxCalculator;

  ConstructorInjectedPayroll(TaxCalculator taxCalculator) {
    this.taxCalculator = taxCalculator;
  }

  double net(double gross) {
    return gross - taxCalculator.tax(gross);
  }
}

public class FieldInjectionTestApp {
  public static void main(String[] args) {
    // A test double: a calculator with a fixed 10% rate
    TaxCalculator tenPercent = new TaxCalculator() {
      @Override
      double tax(double gross) {
        return gross * 0.1;
      }
    };

    System.out.println("Constructor injection: net = " + new ConstructorInjectedPayroll(tenPercent).net(1000));

    try {
      // There is no way to pass tenPercent in - the field is private and has no setter
      System.out.println("Field injection: net = " + new FieldInjectedPayroll().net(1000));
    } catch (NullPointerException e) {
      System.out.println("Field injection: " + e.getMessage());
    }
  }
}
 OUTPUT of the above code
Constructor injection: net = 900.0
Field injection: Cannot invoke "com.javabrahman.spring.di.field.TaxCalculator.tax(double)" because "this.taxCalculator" is null

Explanation of the code

  • The constructor-injected class took the test double through its constructor and worked: 10% tax on 1000 leaves 900.
  • The field-injected class could only be created with its empty constructor. Nothing set the private field, so the first call failed with a NullPointerException. The helpful message (a Java 14 feature) names the field.
  • To test a field-injected class you need reflection tools, a mocking framework that writes into private fields, or a full Spring context - all slower and more fragile than calling a constructor.
  • Field injection also hides how many dependencies a class has. A constructor with eight parameters is an obvious sign that a class does too much; eight annotated fields are easy to overlook.

Optional dependencies and ObjectProvider

Besides required = false, Spring offers three ways to make a constructor parameter optional: java.util.Optional<T>, a parameter annotated @Nullable, and ObjectProvider<T>. ObjectProvider is the most flexible: it lets the bean decide at runtime whether, and when, to get the dependency -
Optional and ObjectProvider for dependencies that may be missing
//OptionalDependencyApp.java
package com.javabrahman.spring.di.optional;

import java.util.Optional;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

interface DiscountPolicy {   // no implementation is a bean
  double apply(double price);
}

@Component
class PriceFormatter {
  String format(double price) {
    return String.format("$%.2f", price);
  }
}

@Service
class CheckoutService {
  private final Optional<DiscountPolicy> discountPolicy;
  private final ObjectProvider<PriceFormatter> formatterProvider;

  CheckoutService(Optional<DiscountPolicy> discountPolicy, ObjectProvider<PriceFormatter> formatterProvider) {
    this.discountPolicy = discountPolicy;
    this.formatterProvider = formatterProvider;
  }

  String total(double price) {
    double discounted = discountPolicy.map(policy -> policy.apply(price)).orElse(price);
    PriceFormatter formatter = formatterProvider.getIfAvailable(PriceFormatter::new);
    return "Discount policy present: " + discountPolicy.isPresent() + ", total " + formatter.format(discounted);
  }
}

@Configuration
@ComponentScan
class AppConfig {
}

public class OptionalDependencyApp {
  public static void main(String[] args) {
    try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
      System.out.println(context.getBean(CheckoutService.class).total(59.5));
    }
  }
}
 OUTPUT of the above code
Discount policy present: false, total $59.50

Explanation of the code

  • There is no DiscountPolicy bean, so Spring injected an empty Optional instead of failing at startup. The constructor and the final field remain - optional dependencies do not force setter injection.
  • ObjectProvider<PriceFormatter> is injected even though the bean is only fetched later. getIfAvailable(PriceFormatter::new) returns the bean if it exists and otherwise uses the supplied fallback.
  • ObjectProvider also has getObject(), getIfUnique() and stream(), and it fetches the bean each time it is called - useful for beans that should be looked up lazily, or for getting a fresh prototype bean (covered in the tutorial on bean scopesClick to read tutorial: Spring Bean Scopes: Singleton, Prototype, Request, Session).

Injecting all beans of a type

When several beans implement the same interface, a constructor that asks for one of them fails, as we saw in the tutorial on the IoC containerClick to read tutorial: Spring IoC Container and ApplicationContext Explained. A constructor that asks for a List or a Map of that type gets all of them - the basis of plug-in designs, where adding a class adds a feature -
Injecting every bean of a type as a List and as a Map
//CollectionInjectionApp.java
package com.javabrahman.spring.di.collections;

import java.util.List;
import java.util.Map;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

interface Notifier {
  void send(String message);
}

@Component
@Order(2)
class EmailNotifier implements Notifier {
  public void send(String message) {
    System.out.println("  email: " + message);
  }
}

@Component
@Order(1)
class SmsNotifier implements Notifier {
  public void send(String message) {
    System.out.println("  sms:   " + message);
  }
}

@Component
@Order(3)
class SlackNotifier implements Notifier {
  public void send(String message) {
    System.out.println("  slack: " + message);
  }
}

@Service
class AlertService {
  private final List<Notifier> notifiers;
  private final Map<String, Notifier> notifiersByName;

  AlertService(List<Notifier> notifiers, Map<String, Notifier> notifiersByName) {
    this.notifiers = notifiers;
    this.notifiersByName = notifiersByName;
  }

  void alertEveryone(String message) {
    System.out.println("Sending to all " + notifiers.size() + " notifiers, in @Order order:");
    notifiers.forEach(n -> n.send(message));
    System.out.println("Bean names in the map: " + notifiersByName.keySet());
    notifiersByName.get("emailNotifier").send("only by email");
  }
}

@Configuration
@ComponentScan
class AppConfig {
}

public class CollectionInjectionApp {
  public static void main(String[] args) {
    try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
      context.getBean(AlertService.class).alertEveryone("Server disk 90% full");
    }
  }
}
 OUTPUT of the above code
Sending to all 3 notifiers, in @Order order:
  sms:   Server disk 90% full
  email: Server disk 90% full
  slack: Server disk 90% full
Bean names in the map: [emailNotifier, slackNotifier, smsNotifier]
  email: only by email

Explanation of the code

  • List<Notifier> received all three beans. @Order sets their order in the list - lower values first - so SMS came first even though it is not first in the file.
  • Map<String, Notifier> received the same beans keyed by bean name, so one can be chosen by name at runtime.
  • Adding a fourth Notifier class is enough for AlertService to start using it - no other change is needed.

Classes with more than one constructor

If a bean class has several constructors, Spring cannot guess which one to use. Mark the one it should use with @Autowired; without a marked constructor, Spring falls back to the no-argument constructor if there is one, and fails if there is not -
Telling Spring which of several constructors to use
@Service
class ExportService {
  private final Formatter formatter;

  @Autowired   // Spring uses this one
  ExportService(Formatter formatter) {
    this.formatter = formatter;
  }

  ExportService() {   // used by older code
    this(new PlainFormatter());
  }
}

Circular dependencies

A circular dependency is when bean A needs bean B and bean B needs bean A. With constructor injection, Spring cannot create either one first, so startup fails and the error names the cycle -
A circular dependency between two constructor-injected beans
//CircularDependencyApp.java
package com.javabrahman.spring.di.circular;

import org.springframework.beans.factory.BeanCurrentlyInCreationException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Service;

@Service
class OrderService {
  OrderService(InvoiceService invoiceService) {
  }
}

@Service
class InvoiceService {
  InvoiceService(OrderService orderService) {
  }
}

@Configuration
@ComponentScan
class AppConfig {
}

public class CircularDependencyApp {
  public static void main(String[] args) {
    try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
      System.out.println("Started with " + context.getBeanDefinitionCount() + " beans");
    } catch (Exception e) {
      Throwable cause = e;
      while (!(cause instanceof BeanCurrentlyInCreationException) && cause.getCause() != null) {
        cause = cause.getCause();
      }
      System.out.println(cause.getClass().getSimpleName() + ": " + cause.getMessage());
    }
  }
}
 OUTPUT of the above code
BeanCurrentlyInCreationException: Error creating bean with name 'invoiceService': Requested bean is currently in creation: Is there an unresolvable circular reference or an asynchronous initialization dependency?

Explanation of the code

  • To create InvoiceService, Spring needs an OrderService, whose constructor needs the InvoiceService that is still being created. Spring detects the cycle and throws BeanCurrentlyInCreationException, wrapped in the startup exception; the example unwraps it.
  • With setter or field injection, the plain Spring Framework can resolve such a cycle by creating both objects first and injecting them afterwards. That hides the design problem rather than solving it. Spring Boot refuses circular references by default even then, unless spring.main.allow-circular-references=true is set.
  • The real fix is a better design: move the shared logic into a third bean that both use, or let one side publish an event instead of calling the other.

Which injection style to use

  • Constructor injection - the default for required dependencies. Fields can be final, the object is complete as soon as it exists, the dependencies are visible in one place, unit tests just call the constructor, and circular dependencies are caught at startup. The Spring team recommends it.
  • Setter injection - for genuinely optional dependencies that have a sensible default, or for the rare dependency that must be changeable after construction.
  • Field injection - avoid in application code. It is acceptable in test classes, where the test framework creates the object, such as @Autowired fields in a Spring test.
  • For optional dependencies in a constructor, prefer Optional<T> or ObjectProvider<T> over switching to setters.

Summary

In this tutorial we first saw what Spring does when it injects a dependency, and the three places a bean can ask for one. We wrote a service with constructor injection, then with setter injection, including an optional dependency that was skipped. We saw that field injection works inside the container but leaves a class impossible to create correctly in a plain unit test. Next we made constructor parameters optional with Optional and ObjectProvider, injected all beans of a type as an ordered list and as a map, and told Spring which of several constructors to use. Lastly, we saw a circular dependency fail fast with constructor injection, and concluded that constructor injection should be the default. The next tutorial covers how many instances of a bean exist and for how long: Spring bean scopesClick to read tutorial: Spring Bean Scopes: Singleton, Prototype, Request, Session.