Command Palette

Search for a command to run...

[Spring Boot Basics] Constructor, Setter and Field Injection in Spring, with @Qualifier and @Primary

Spring can hand a dependency to your object in three places: a constructor parameter, a setter method, or straight into a private field. All three work. They differ in exactly one thing — the moment the value arrives relative to the object's own construction — and every other difference between them, including which ones let you write a plain unit test and which ones let a circular dependency through, follows from that single fact.

Everything below was run on Spring Boot 4.1.1 (Spring Framework 7.0.9) with Java 21.0.6 and Gradle 9.7.1. Every console line, exception and failure banner is copied out of the terminal, including the ones that were reproduced on purpose.

Three injection points: constructor with a final field, setter and field starting null

The example is deliberately small: an AuditLog component and three services that each want one, written the three different ways.

Constructor, setter and field: the three injection points

Here are the three, side by side. The dependency is the same in all three cases; only the way in changes.

ByConstructor.java
@Service
public class ByConstructor {
 
    private final AuditLog auditLog;
 
    public ByConstructor(AuditLog auditLog) {
        this.auditLog = auditLog;
    }
 
    public void use() { auditLog.record("constructor injection"); }
}
BySetter.java
@Service
public class BySetter {
 
    private AuditLog auditLog;
 
    @Autowired
    public void setAuditLog(AuditLog auditLog) {
        this.auditLog = auditLog;
    }
 
    public void use() { auditLog.record("setter injection"); }
}
ByField.java
@Service
public class ByField {
 
    @Autowired
    private AuditLog auditLog;
 
    public void use() { auditLog.record("field injection"); }
}

Three things are already visible without running anything. Only the first can declare auditLog as final. Only the first has no @Autowired anywhere. And only the first would still compile if you deleted Spring from the classpath and kept the class.

The trace: when each one gets its value

Add a print to each constructor and to the setter, and the container tells you the rest. This is the real output:

Text
[constructor] inside the constructor, auditLog = injected
[field]       inside the constructor, auditLog = null
[setter]      inside the constructor, auditLog = null
[setter]      inside setAuditLog(),   auditLog = injected
---- context is up, every bean is complete ----
AUDIT: constructor injection
AUDIT: setter injection
AUDIT: field injection

Read the first four lines carefully, because they are the whole article in miniature:

  • Constructor injection. The container resolves AuditLog first, then calls new ByConstructor(auditLog). The value is present from the first statement of the constructor body. There is no instant at which a ByConstructor exists without its dependency.
  • Setter injection. The container calls the no-argument constructor, gets back an object whose field is null, and only then calls setAuditLog(...). Between those two calls a fully-typed BySetter exists with a null field.
  • Field injection. Identical timing to setter injection, but the write happens by reflection directly into the private field instead of through a method. The reflective write is why no setter is needed and why the field cannot be final.

The same three moments traced through constructor, setter and field injection

By the time the context has started — the ---- context is up ---- line — all three are complete and all three work. The difference only exists during startup. It just happens that a great deal of what goes wrong with dependency injection goes wrong during startup.

Why is @Autowired optional on a constructor?

ByConstructor above has no annotation on its constructor and is still injected. Since Spring 4.3, a class with exactly one constructor does not need @Autowired on it — the container has no choice to make, so it uses that constructor and resolves every parameter as a dependency. This is why modern Spring code has almost no @Autowired in it at all.

"Exactly one" is the entire rule. Add a second constructor and the container is suddenly being asked to choose, and it refuses to guess.

Two constructors, and the silence that follows

NotificationService.java
@Service
public class NotificationService {
 
    private final AuditLog auditLog;
    private final Mailer mailer;
 
    public NotificationService(AuditLog auditLog) {
        this(auditLog, null);
    }
 
    public NotificationService(AuditLog auditLog, Mailer mailer) {
        this.auditLog = auditLog;
        this.mailer = mailer;
    }
 
    public void notifyUser(String to) { /* ... */ }
}

Two constructors, neither annotated. The application does not start:

Text
org.springframework.beans.factory.BeanCreationException: Error creating bean with name
'notificationService' defined in file [.../com/example/demo/NotificationService.class]:
Failed to instantiate [com.example.demo.NotificationService]: No default constructor found
 
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate
[com.example.demo.NotificationService]: No default constructor found
 
Caused by: java.lang.NoSuchMethodException: com.example.demo.NotificationService.<init>()

That message misleads almost everyone who meets it. It is not saying "add a no-argument constructor". It is saying: I found more than one candidate constructor, none of them was marked, so I fell back to looking for a default constructor and there isn't one either. The fix is to mark the one you want:

NotificationService.java
    @Autowired
    public NotificationService(AuditLog auditLog, Mailer mailer) {
        this.auditLog = auditLog;
        this.mailer = mailer;
    }
Text
AUDIT: notify ada@example.com
MAIL -> ada@example.com

There is a nastier version of the same situation. If one of the two constructors is a no-argument constructor, the fallback succeeds — and nothing complains at all:

NotificationService.java
    public NotificationService() {
        System.out.println("chosen constructor: NotificationService()");
    }
 
    public NotificationService(AuditLog auditLog, Mailer mailer) {
        System.out.println("chosen constructor: NotificationService(AuditLog, Mailer)");
        this.auditLog = auditLog;
        this.mailer = mailer;
    }
Text
chosen constructor: NotificationService()
Exception in thread "main" java.lang.NullPointerException: Cannot invoke
"com.example.demo.AuditLog.record(String)" because "this.auditLog" is null
	at com.example.demo.NotificationService.notifyUser(NotificationService.java:22)

The context started cleanly. The bean exists. Every field in it is null, and you find out at the first request. One constructor needs no annotation; two constructors need exactly one @Autowired.

The case for constructor injection

The Spring reference documentation recommends constructor injection, and the recommendation is easy to repeat and easy to under-explain. Here are the four reasons, each demonstrated rather than asserted.

final fields and an object that is complete the moment it exists

Constructor injection lets the field be final. That is not decoration: it means the field is assigned exactly once, during construction, and the compiler enforces it. Nobody can null it out later, no @PostConstruct can be forgotten, and any thread that gets a reference to the object is guaranteed to see the fully initialised field.

Field injection cannot do this, and the compiler says so before Spring is ever involved:

FinalFieldService.java
@Service
public class FinalFieldService {
 
    @Autowired
    private final AuditLog auditLog;
 
    public void use() { auditLog.record("final field"); }
}
Text
FinalFieldService.java:10: error: variable auditLog not initialized in the default constructor

There is no trick that recovers this. A final field must be assigned by the end of every constructor, and the whole point of field injection is that the write happens after the constructor has already returned. final and field injection are mutually exclusive.

The null window that field injection leaves open

"The field is null for a moment" sounds theoretical until you write something in the constructor. This class is not contrived — precomputing a value in the constructor is ordinary Java:

ReportService.java
@Service
public class ReportService {
 
    @Autowired
    private AuditLog auditLog;
 
    private final String banner;
 
    public ReportService() {
        auditLog.record("ReportService starting up");
        this.banner = "reports ready";
    }
}
Text
org.springframework.beans.factory.BeanCreationException: Error creating bean with name
'reportService' defined in file [.../com/example/demo/ReportService.class]:
Failed to instantiate [com.example.demo.ReportService]: Constructor threw exception
 
Caused by: java.lang.NullPointerException: Cannot invoke
"com.example.demo.AuditLog.record(String)" because "this.auditLog" is null
	at com.example.demo.ReportService.<init>(ReportService.java:15) ~[main/:na]

The dependency is declared, the bean it needs exists in the context, and it is still null — because the container has not got to the reflective write yet. Nothing about the code reads as wrong; the ordering is invisible.

Now the same mistake with constructor injection. There is no mistake to make: the parameter is in scope, non-null, before the first statement of the body runs. And when the dependency genuinely is missing, the failure is not an NPE somewhere inside your code — it is a diagnosis, printed before the application starts:

Text
***************************
APPLICATION FAILED TO START
***************************
 
Description:
 
Parameter 0 of constructor in com.example.demo.CartService required a bean of type
'com.example.demo.PricingClient' that could not be found.
 
 
Action:
 
Consider defining a bean of type 'com.example.demo.PricingClient' in your configuration.

To be fair to field injection: a missing bean fails at startup there too, and the message is just as good, naming the field and the annotation on it.

Text
Description:
 
Field pricing in com.example.demo.CartService required a bean of type
'com.example.demo.PricingClient' that could not be found.
 
The injection point has the following annotations:
	- @org.springframework.beans.factory.annotation.Autowired(required=true)

The difference is not "startup versus runtime" for a missing bean. It is that constructor injection makes the half-built object unrepresentable, so the NPE window above simply does not exist.

Dependencies you can count in the signature

A class with eight dependencies is a class doing eight things. With constructor injection it looks like what it is:

Java
public OrderService(OrderRepository orders, PaymentGateway payments, InventoryClient inventory,
                    PricingService pricing, EmailSender email, AuditLog audit,
                    MetricsRecorder metrics, FeatureFlags flags) {

Nobody reads that and thinks it is fine. With field injection, the same eight dependencies are eight one-line annotations spread down the class body, and each individual line looks harmless — which is precisely the problem. The pain of a long parameter list is useful pain: it is the design feedback that tells you the class wants splitting. Field injection anaesthetises it.

A unit test with no Spring context in it

This is the argument that settles it in practice. A constructor-injected class is an ordinary Java class, so a test can build it directly with a stub and never mention Spring:

CheckoutServiceTest.java
class CheckoutServiceTest {
 
    private final PaymentGateway stub = amountCents -> "stub:" + amountCents;
 
    @Test
    void constructor_injected_service_needs_no_container() {
        CheckoutService service = new CheckoutService(stub);
 
        assertThat(service.checkout(2500)).isEqualTo("stub:2500");
    }
 
    @Test
    void field_injected_service_cannot_be_built_the_same_way() {
        FieldCheckoutService service = new FieldCheckoutService();
 
        assertThatThrownBy(() -> service.checkout(2500))
                .isInstanceOf(NullPointerException.class)
                .hasMessage("Cannot invoke \"com.example.demo.PaymentGateway.charge(long)\" "
                        + "because \"this.gateway\" is null");
    }
 
    @Test
    void field_injected_service_needs_reflection_to_test() {
        FieldCheckoutService service = new FieldCheckoutService();
        ReflectionTestUtils.setField(service, "gateway", stub);
 
        assertThat(service.checkout(2500)).isEqualTo("stub:2500");
    }
}

All three pass:

Text
CheckoutServiceTest > field_injected_service_needs_reflection_to_test() PASSED
CheckoutServiceTest > field_injected_service_cannot_be_built_the_same_way() PASSED
CheckoutServiceTest > constructor_injected_service_needs_no_container() PASSED
 
BUILD SUCCESSFUL in 703ms
4 actionable tasks: 4 executed

The middle test is the one to look at. new FieldCheckoutService() compiles, runs, and hands you an object that throws the moment you use it. The third test shows the only way out without a container: ReflectionTestUtils.setField(service, "gateway", stub) — which means your test now depends on the name of a private field, and a rename that the compiler would normally catch silently breaks it instead.

The alternative to reflection is to start a context, and that is not free. The same single assertion, measured from the JUnit XML report over three runs on this machine:

Test styleSuite time
Plain constructor call, no Spring0.021 s, 0.021 s, 0.027 s
@SpringBootTest with the context started0.787 s, 0.849 s, 0.913 s

Roughly forty times, on an application with one bean of interest. Treat the numbers as indicative of the shape rather than as a benchmark — but the shape does not change as the application grows, it gets worse. Constructor injection is what keeps the fast path available.

When is setter injection the right choice?

Setter injection is not a mistake; it is a narrower tool. It earns its place in two situations.

The first is a genuinely optional dependency: something the class works without, where the absence is a supported configuration rather than a failure. The second is a collaborator that can legitimately be replaced after construction — a strategy swapped at runtime, a component reconfigured by an administrative endpoint. Both of those need a mutable field by definition, so final was never on the table anyway.

What setter injection is not for is "this class has too many constructor parameters". That is the design feedback from the previous section; muting it with setters does not make the class smaller.

Optional dependencies: required = false, Optional and ObjectProvider

There are four ways to say "inject this if it exists". Here they are in one class, with no FraudCheck bean registered anywhere:

OptionalDemo.java
@Service
public class OptionalDemo {
 
    private final Optional<FraudCheck> viaOptional;
    private final ObjectProvider<FraudCheck> viaProvider;
    private final @Nullable FraudCheck viaNullable;
 
    @Autowired(required = false)
    private FraudCheck viaField;
 
    private FraudCheck viaSetter;
 
    public OptionalDemo(Optional<FraudCheck> viaOptional,
                        ObjectProvider<FraudCheck> viaProvider,
                        @Nullable FraudCheck viaNullable) {
        this.viaOptional = viaOptional;
        this.viaProvider = viaProvider;
        this.viaNullable = viaNullable;
    }
 
    @Autowired(required = false)
    public void setViaSetter(FraudCheck viaSetter) {
        System.out.println("setViaSetter() was called");
        this.viaSetter = viaSetter;
    }
}
Text
Optional<FraudCheck>       : isPresent=false
ObjectProvider<FraudCheck> : getIfAvailable=null
@Nullable FraudCheck       : null
@Autowired(required=false) field  : null
@Autowired(required=false) setter : null

Note what is missing from that output: setViaSetter() was called never printed. With required = false on a setter, the container does not call the method at all when no candidate exists. That is a real behavioural difference from the field form, and it is the reason a required = false setter must never be the only place a default is applied — the method may simply never run.

Register one implementation and everything fills in, the setter included:

Text
setViaSetter() was called
Optional<FraudCheck>       : isPresent=true
ObjectProvider<FraudCheck> : getIfAvailable=VelocityFraudCheck
@Nullable FraudCheck       : VelocityFraudCheck
@Autowired(required=false) field  : VelocityFraudCheck
@Autowired(required=false) setter : VelocityFraudCheck

Which to prefer:

Way to say "optional"Use it when
Optional<T> as a constructor parameterThe default choice. The type itself documents the optionality, the field can stay final, and the compiler forces every caller to handle the empty case.
@Nullable T as a constructor parameterYou want a plain T field rather than an Optional one. Spring Framework 7 uses JSpecify's org.jspecify.annotations.Nullable, which is on the classpath already.
ObjectProvider<T>You need more than presence: getIfAvailable(), getIfUnique(), stream() and orderedStream() all live here, and resolution is deferred until you ask.
@Autowired(required = false)Legacy code and nothing else. On a field it leaves you a bare null; on a setter the method may never be called.

Two beans of the same type: NoUniqueBeanDefinitionException

Everything so far assumed one candidate per type. The interesting half of dependency injection starts when there are two. One interface, two implementations, both components:

Java
public interface PaymentGateway {
    String charge(long amountCents);
}
 
@Component
public class StripeGateway implements PaymentGateway {
    @Override
    public String charge(long amountCents) { return "stripe:" + amountCents; }
}
 
@Component
public class PaypalGateway implements PaymentGateway {
    @Override
    public String charge(long amountCents) { return "paypal:" + amountCents; }
}
 
@Service
public class CheckoutService {
 
    private final PaymentGateway gateway;
 
    public CheckoutService(PaymentGateway gateway) {
        this.gateway = gateway;
    }
 
    public String checkout(long amountCents) { return gateway.charge(amountCents); }
}

The application does not start. The underlying exception is the one whose name everybody eventually learns:

Text
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with
name 'checkoutService' defined in file [.../com/example/demo/CheckoutService.class]:
Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type
'com.example.demo.PaymentGateway' available: expected single matching bean but found 2:
paypalGateway,stripeGateway

And Spring Boot's failure analyser turns it into this, which is what you will actually see on the console:

Text
***************************
APPLICATION FAILED TO START
***************************
 
Description:
 
Parameter 0 of constructor in com.example.demo.CheckoutService required a single bean, but 2 were found:
	- paypalGateway: defined in file [.../com/example/demo/PaypalGateway.class]
	- stripeGateway: defined in file [.../com/example/demo/StripeGateway.class]
 
This may be due to missing parameter name information
 
Action:
 
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
 
Ensure that your compiler is configured to use the '-parameters' flag.
You may need to update both your build tool settings as well as your IDE.
(See https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-6.1-Release-Notes#parameter-name-retention)

The -parameters hint at the bottom is printed unconditionally by the analyser and is usually a red herring on a Gradle or Maven Spring Boot build, where the plugin already turns that flag on. It matters for exactly one of the four fixes below, and the section on that fix proves both halves.

The resolution algorithm is short and worth memorising: match by type, and if more than one candidate survives, break the tie by @Primary, then by jakarta.annotation.Priority, then by matching the bean name against the injection point's name. If nothing breaks the tie, refuse to start.

One injection point, two candidate beans, and the three ways the container decides

Fix 1: mark one bean @Primary

StripeGateway.java
@Component
@Primary
public class StripeGateway implements PaymentGateway { /* ... */ }
Text
checkout -> stripe:2500

@Primary sits on the bean, not on the injection point, and answers a question about the whole application: "when nobody says otherwise, which one?" That makes it right for a genuine default — the main DataSource, the ordinary ObjectMapper, the gateway ninety per cent of the code should use. It is wrong as a way to silence one error message, because it silently answers for every other injection point too, including ones written next year.

Two beans both marked @Primary is back to square one, with a more specific message:

Text
NoUniqueBeanDefinitionException: No qualifying bean of type 'com.example.demo.PaymentGateway'
available: more than one 'primary' bean found among candidates: [paypalGateway, stripeGateway]

Fix 2: @Qualifier at the injection point

CheckoutService.java
    public CheckoutService(@Qualifier("paypalGateway") PaymentGateway gateway) {
        this.gateway = gateway;
    }
Text
checkout -> paypal:2500

@Qualifier is the opposite of @Primary: it lives at the injection point and decides one wiring only. The string matches a qualifier value declared on a bean, and — as here — falls back to matching the bean name. Both are worth knowing, because naming the bean is what most examples do and declaring a qualifier is what survives a class rename:

StripeGateway.java
@Component
@Qualifier("cards")
public class StripeGateway implements PaymentGateway { /* ... */ }
CheckoutService.java
    public CheckoutService(@Qualifier("cards") PaymentGateway gateway) { /* ... */ }
Text
checkout -> stripe:2500

This form is the better one. @Qualifier("stripeGateway") couples the caller to the implementation's class name; @Qualifier("cards") couples it to a role, which is the thing the caller actually cares about.

@Qualifier works at every injection point, not just constructors — on a field, and on a setter's parameter:

Java
    @Autowired
    @Qualifier("stripeGateway")
    private PaymentGateway gateway;
 
    @Autowired
    public void setOther(@Qualifier("paypalGateway") PaymentGateway other) {
        this.other = other;
    }
Text
checkout -> stripe:2500 / paypal:2500

Fix 3: match the parameter name to the bean name

The last step of the resolution algorithm is a name match, and you can aim it deliberately. Rename nothing but the parameter:

CheckoutService.java
    public CheckoutService(PaymentGateway stripeGateway) {
        this.gateway = stripeGateway;
    }
Text
checkout -> stripe:2500

No annotation at all, and it resolves. This is also why @Autowired private PaymentGateway stripeGateway; "just works" in so many tutorials — the field name is doing the qualifying.

Now the caveat, which is what that -parameters hint in the failure banner was about. Parameter names only survive into the class file when javac is given -parameters; without it, the constructor's parameter is called arg0 at runtime and there is nothing to match. The Spring Boot Gradle and Maven plugins add the flag for you — javap -v on the class above shows the attribute:

Text
    MethodParameters:
      Name                           Flags
      stripeGateway

Take the flag away, change nothing else, and the MethodParameters attribute disappears along with the wiring:

build.gradle
tasks.withType(JavaCompile).configureEach {
	options.compilerArgs.remove('-parameters')
}
Text
Description:
 
Parameter 0 of constructor in com.example.demo.CheckoutService required a single bean, but 2 were found:
	- paypalGateway: ...
	- stripeGateway: ...

So the mechanism is real but fragile: it depends on a compiler flag, it breaks under any obfuscator, and a rename that an IDE treats as safe — renaming a parameter — silently changes which bean is wired. Use it knowingly, or not at all.

Fix 4: a custom qualifier annotation

@Qualifier is a meta-annotation, so you can build a typed qualifier of your own out of it:

CardPayments.java
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER })
public @interface CardPayments {
}
Java
@Component
@CardPayments
public class StripeGateway implements PaymentGateway { /* ... */ }
 
@Component
@WalletPayments
public class PaypalGateway implements PaymentGateway { /* ... */ }
CheckoutService.java
@Service
public class CheckoutService {
 
    private final PaymentGateway cards;
    private final PaymentGateway wallets;
 
    public CheckoutService(@CardPayments PaymentGateway cards,
                           @WalletPayments PaymentGateway wallets) {
        this.cards = cards;
        this.wallets = wallets;
    }
}
Text
checkout -> stripe:2500 / paypal:2500

This is @Qualifier("cards") with the magic string replaced by a type. A typo is now a compile error instead of a startup failure, the IDE can find every usage, and the annotation has a Javadoc page where the meaning of "card payments" can be written down. On anything larger than a toy, this is the version to reach for.

@Primary and @Qualifier together

They can coexist, and the precedence is worth seeing rather than assuming. StripeGateway is @Primary; one of the two parameters below asks for something else:

CheckoutService.java
    public CheckoutService(PaymentGateway byDefault,
                           @Qualifier("paypalGateway") PaymentGateway byQualifier) {
        this.byDefault = byDefault;
        this.byQualifier = byQualifier;
    }
Text
no qualifier -> stripe:2500
@Qualifier   -> paypal:2500

@Qualifier wins. @Primary is only consulted when the injection point expressed no preference, which is exactly the behaviour you want: a sensible default for everyone, overridable at the one place that needs the other thing.

Injecting every implementation at once

Sometimes the answer to "which implementation?" is "all of them" — a set of validators, a chain of handlers, a registry keyed by name. Declare a collection of the interface type and the container fills it:

GatewayRegistry.java
@Service
public class GatewayRegistry {
 
    private final List<PaymentGateway> all;
    private final Map<String, PaymentGateway> byName;
 
    public GatewayRegistry(List<PaymentGateway> all, Map<String, PaymentGateway> byName) {
        this.all = all;
        this.byName = byName;
    }
}

With three implementations on the classpath:

Text
List<PaymentGateway> size = 3
  BankTransferGateway -> bank:100
  PaypalGateway -> paypal:100
  StripeGateway -> stripe:100
Map<String, PaymentGateway> keys = [bankTransferGateway, paypalGateway, stripeGateway]
  bankTransferGateway -> bank:100
  paypalGateway -> paypal:100
  stripeGateway -> stripe:100

A List<T> gets every bean assignable to T. A Map<String, T> gets the same beans keyed by bean name — note that the key type must be String, because bean names are strings. This is the idiomatic way to let a feature be extended by adding a class: the registry does not change, the new @Component joins the collection automatically.

Ordering a List with @Order

The order in that list was not chosen by you. It happens to be scan order here, and scan order is not a contract — it can change when you rename a class, split a package, or repackage into a jar. If order matters, say so:

Java
@Component
@Order(1)
public class StripeGateway implements PaymentGateway { /* ... */ }
 
@Component
@Order(2)
public class PaypalGateway implements PaymentGateway { /* ... */ }
 
@Component
@Order(3)
public class BankTransferGateway implements PaymentGateway { /* ... */ }
Text
List<PaymentGateway> size = 3
  StripeGateway -> stripe:100
  PaypalGateway -> paypal:100
  BankTransferGateway -> bank:100
Map<String, PaymentGateway> keys = [bankTransferGateway, paypalGateway, stripeGateway]

Two details in that output. The List is now in the declared order — lower numbers first — and a class can implement org.springframework.core.Ordered instead of using the annotation if the value has to be computed. And the Map is unchanged: sorting is applied to arrays and collections, not to maps, so a Map<String, T> gives you registration order and you should treat it as unordered.

ObjectProvider exposes the same choice explicitly, which is the clearest way to show what is going on:

GatewayRegistry.java
    public GatewayRegistry(ObjectProvider<PaymentGateway> gateways) {
        this.gateways = gateways;
    }
Text
stream()        : [BankTransferGateway, PaypalGateway, StripeGateway]
orderedStream() : [StripeGateway, PaypalGateway, BankTransferGateway]
getIfUnique()   : null

stream() gives you registration order, orderedStream() applies @Order, and getIfUnique() returns null rather than throwing when the type is ambiguous — a fifth, programmatic way of dealing with multiple candidates.

Circular dependencies

Two beans that each need the other is the one failure mode where the choice of injection point changes not just the error message but whether the application starts at all.

OrderService.java
@Service
public class OrderService {
 
    private final InvoiceService invoiceService;
 
    public OrderService(InvoiceService invoiceService) {
        this.invoiceService = invoiceService;
    }
}
InvoiceService.java
@Service
public class InvoiceService {
 
    private final OrderService orderService;
 
    public InvoiceService(OrderService orderService) {
        this.orderService = orderService;
    }
}

Spring Boot draws the cycle for you:

Text
***************************
APPLICATION FAILED TO START
***************************
 
Description:
 
The dependencies of some of the beans in the application context form a cycle:
 
┌─────┐
|  invoiceService defined in file [.../com/example/demo/InvoiceService.class]
↑     ↓
|  orderService defined in file [.../com/example/demo/OrderService.class]
└─────┘
 
 
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.

This is not a policy decision by Spring, it is arithmetic. To construct orderService the container needs a finished invoiceService; to construct invoiceService it needs a finished orderService. Neither can be first. There is nothing a configuration property could do about it, and the property named in that Action: block proves it — turn it on and the very same cycle fails again, with a different last line:

application.properties
spring.main.allow-circular-references=true
Text
Description:
 
The dependencies of some of the beans in the application context form a cycle:
 
┌─────┐
|  invoiceService defined in file [.../com/example/demo/InvoiceService.class]
↑     ↓
|  orderService defined in file [.../com/example/demo/OrderService.class]
└─────┘
 
 
Action:
 
Despite circular references being allowed, the dependency cycle between beans could not be broken. Update your application to remove the dependency cycle.

Now rewrite exactly the same two classes with field injection:

OrderService.java
@Service
public class OrderService {
 
    @Autowired
    private InvoiceService invoiceService;
}
InvoiceService.java
@Service
public class InvoiceService {
 
    @Autowired
    private OrderService orderService;
}

The cycle is now physically resolvable, because both objects can be created empty and the fields written afterwards — an early reference to the half-built orderService can be handed to invoiceService and patched up later. Spring Boot still refuses it by default (this has been the default since Boot 2.6), with the same banner naming the two fields instead of the two classes:

Text
┌─────┐
|  invoiceService (field private com.example.demo.OrderService com.example.demo.InvoiceService.orderService)
↑     ↓
|  orderService (field private com.example.demo.InvoiceService com.example.demo.OrderService.invoiceService)
└─────┘

But here the property really does work:

application.properties
spring.main.allow-circular-references=true
Text
order:sku-1 invoice:sku-1

A constructor cycle the container cannot build, next to a field cycle it can

⚠️ That property is a migration aid, not a design option. It applies globally, it makes every future cycle in the application silent, and it leaves you with beans that hold references to objects that were incompletely initialised when the reference was taken. If you add it, add a dated comment next to it saying which cycle it is for.

What actually fixes a cycle

A cycle is information: it says two classes each know something the other needs, which means the boundary between them is in the wrong place. The real fixes, roughly in order of how often they are the right one:

  • Move the shared logic into a third class that both depend on. The cycle usually exists because a piece of behaviour was assigned to one of the two arbitrarily.
  • Invert one direction with an event. If OrderService only calls InvoiceService to tell it something happened, publish an application event instead of holding a reference.
  • Extract an interface that one side depends on and the other implements, so the compile-time dependency points one way even though the call goes the other.
  • Merge them. Two classes that cannot be constructed independently are frequently one class that was split too early.

@Lazy on one of the two injection points will also break a constructor cycle, by injecting a proxy that resolves the real bean on first use — and the proxy is not hypothetical, it has a class name:

OrderService.java
    public OrderService(@Lazy InvoiceService invoiceService) {
Text
OrderService ctor, invoiceService is a InvoiceService$$SpringCGLIB$$0
order:sku-1 invoice:sku-1

It works, and it is still the same cycle with a CGLIB proxy standing in front of it. Reach for it only when the two classes are not yours to change.

And note what constructor injection did for you here: it turned a design problem into a startup failure with both class names printed, on the first run, on your machine. Field injection would have taken the same design and started up fine.

Constructor vs setter vs field

ConstructorSetterField
@Autowired neededNo, with exactly one constructorYes, on the methodYes, on the field
When the value arrivesBefore the constructor body runsAfter construction, via the methodAfter construction, via reflection
Field can be finalYesNoNo
Object ever observable half-builtNeverYes, between construction and the setterYes, between construction and the write
Visible in the public APIYes — the constructor signatureYes — the setterNo
Too many dependenciesObvious, a long parameter listHiddenHidden
Plain JUnit test without SpringYes, new it with stubsYes, new then call the settersOnly with ReflectionTestUtils or a container
Optional dependencyOptional<T>, @Nullable, ObjectProvider<T>@Autowired(required = false) — method may never be called@Autowired(required = false) — leaves null
Circular dependencyFails at startup, always, property or notAllowed if the property is setAllowed if the property is set
Use it forEverything mandatory. The default.Genuinely optional or replaceable collaboratorsNothing in production code

FAQ

Should I use Lombok's @RequiredArgsConstructor?

Yes, and it is genuinely constructor injection: Lombok generates a constructor taking every final field, and with a single constructor Spring needs no @Autowired. javap on the generated class confirms there is nothing special about it:

Text
public class com.example.demo.CheckoutService {
  private final com.example.demo.PaymentGateway gateway;
  public com.example.demo.CheckoutService(com.example.demo.PaymentGateway);
}

One trap is worth knowing. Annotations on the field are not copied to the generated constructor parameter by default, so a @Qualifier on a final field is ignored and you get the ambiguity failure back:

Text
Parameter 0 of constructor in com.example.demo.CheckoutService required a single bean, but 2 were found:

The fix is a lombok.config file at the project root, after which the qualifier is copied and the wiring resolves:

Text
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier

Can @Qualifier go on a setter or a field too?

Yes. On a field, put @Autowired and @Qualifier together on the field. On a setter, @Autowired goes on the method and @Qualifier on the parameter — that is the only form that works when the setter takes more than one argument, and it is the form to use habitually.

What happens if two beans are both marked @Primary?

The ambiguity comes straight back, with a message that names the specific problem — and note that Spring Boot prints no formatted failure banner for this one, just the exception:

Text
NoUniqueBeanDefinitionException: No qualifying bean of type 'com.example.demo.PaymentGateway'
available: more than one 'primary' bean found among candidates: [paypalGateway, stripeGateway]

@Primary means "the default", and there can only be one default. If two libraries each mark their own bean primary, the fix is a @Qualifier at your injection points, not a third @Primary.

Does @Autowired do anything on a static field?

No. The field stays null and the container logs one line at INFO that is very easy to scroll past:

Text
INFO f.a.AutowiredAnnotationBeanPostProcessor : Autowired annotation is not supported on
static fields: private static com.example.demo.AuditLog com.example.demo.ByConstructor.auditLog

A static field belongs to the class, not to a bean, so there is no instance for the container to populate. If you are reaching for a static field to get at a bean from somewhere that is not managed, the real fix is to make that code a bean too.

Is field injection ever acceptable?

In test classes, yes — @Autowired on a field in a @SpringBootTest is conventional, the object is built by the framework anyway, and none of the arguments above apply. In @Configuration classes it is at least defensible. In production service, repository and controller classes, there is no case for it that constructor injection does not serve better, and the final field alone is worth the two extra lines.

Why does my class have eight constructor parameters?

Because it has eight dependencies, and the constructor is the only place that shows it. The parameter list is a symptom, not the disease — switching to field injection hides the symptom and leaves the class exactly as complicated. Look for a cluster of parameters that are always used together and extract them into one collaborator, or split the class along the seam where different parameters are used by different methods.

Conclusion

There are three injection points and one thing that separates them: constructor injection supplies the value before the object exists, setter and field injection supply it afterwards. From that one fact follow final fields, an object that can never be observed half-built, a dependency count you cannot hide, a JUnit test that runs in 21 milliseconds because it never starts a context, and a circular dependency that fails loudly on the first run instead of lurking. @Autowired is optional on a single constructor and mandatory the moment there are two. When one type has several beans, resolution goes type, then @Primary, then @Priority, then name — and @Qualifier at the injection point overrules all of it, with a custom qualifier annotation being the version that survives a refactor. Want them all? Ask for a List or a Map, and add @Order if the order is part of the contract.

The next article moves from the injection point to the declaration: @Configuration and @Bean — factory methods that register beans the container could never have scanned for, and when to use them instead of a stereotype annotation.

Related Posts

[Spring Boot Basics] IoC and Dependency Injection in Spring: Why You Stop Calling new

The idea the whole framework rests on, demonstrated on Spring Boot 4.1.1 and Java 21: a four-class object graph built with new at every level and the three failures that follow, Inversion of Control and Dependency Injection named separately, the same graph hand-wired in main with no framework at all, then wired by the Spring container with the injected instance identities printed to prove it, plus a JUnit 5 test with a hand-made stub, an implementation swapped without touching its consumer, and an honest list of what the container costs you.

[Spring Boot Basics] Server-Side Rendering with Thymeleaf in Spring Boot: Templates, Forms and Validation

Server-side rendering with Thymeleaf 3.1.5 in Spring Boot 4.1.1, checked on a running application: when SSR beats a JSON API, how a view name becomes classpath:/templates/products/list.html, the five standard expressions, th:text versus th:utext and XSS, th:each with #numbers and #temporals, a validated create form with th:field and th:errors, where BindingResult must go, Post/Redirect/Get with flash attributes, fragments, static CSS, what spring.thymeleaf.cache really changes, and HTML error pages.

[Spring Boot Basics] application.properties vs application.yml in Spring Boot: Syntax and @Value

application.properties and application.yml in Spring Boot 4.1.1, checked by running them: the .properties syntax rules, the ISO-8859-1 default that mangles Vietnamese text, YAML nesting and the SnakeYAML 2.6 values that silently change type, which file wins when both exist, placeholders and random values, and @Value with defaults, type conversion, SpEL and the Environment.

[Spring Boot Basics] Spring Bean Scopes and the Bean Lifecycle: singleton, prototype, @PostConstruct and @PreDestroy

Spring bean scopes and the bean lifecycle proved by running them on Spring Boot 4.1.1: why singleton means one per container and not one per JVM, singleton vs prototype vs request vs session vs application with real instance counts from curl, why @PreDestroy never fires on a prototype, the singleton-holds-a-prototype trap and the ObjectProvider, @Lookup and scoped-proxy fixes, the full fourteen-step lifecycle order traced from a real run, and what @Lazy actually costs.