@Primary
Marks one bean as the default choice when several beans match an injection point, so plain injection by type is no longer ambiguous.
@Primary- Added in Spring Boot Spring Framework 3.0
Return value
n/aGives the bean preference when more than one candidate of the same type matches a single-valued injection point.
Errors
Two @Primary beans of the same type make injection ambiguous again and fail startup with NoUniqueBeanDefinitionException.
When a constructor asks for a PaymentGateway and the context holds two, Spring cannot choose and startup fails with NoUniqueBeanDefinitionException: expected single matching bean but found 2. @Primary on one of them resolves this: it becomes the default, and every plain injection of the type gets it. Other beans can still ask for the alternative with @Qualifier.
It goes on a @Component class or on a @Bean method, and it has no attributes.
When it fits
- One implementation is normal and the others are special cases: the main
DataSourceand a reporting replica, the standard executor and a CPU-bound one. - You add a second bean of a type that Spring Boot auto-configures, and existing injection points must keep getting the one you consider main.
If neither bean is the obvious default, do not pick one arbitrarily. Use @Qualifier at each injection point so the choice is visible where it is made.
@Fallback, the opposite
Spring Framework 6.2 added @Fallback: a bean marked with it is only used when no other candidate is left. It suits a default implementation in a shared module that an application may replace by simply declaring its own bean, without touching the original.
Mistakes to avoid
- Two
@Primarybeans of one type. Each was added to fix a different conflict, and together they bring the ambiguity back. - Using
@Primaryon a test or local fake to override the real bean everywhere. A profile or a test-specific bean expresses that intent more safely. - Assuming it affects collection injection. A
List<PaymentGateway>still receives every candidate;@Primaryonly decides single-valued injection points.
Defining a second bean of an auto-configured type, such as DataSource, can also switch off Spring Boot's own, because many auto-configurations back off when any bean of the type exists. Executors are a common case: any Executor bean you declare, such as the executors in the examples, makes Spring Boot skip its own applicationTaskExecutor unless spring.task.execution.mode=force is set. After adding a bean of an auto-configured type, check the conditions report (debug=true) to see which beans Boot still creates.
Example
import java.math.BigDecimal;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
interface PaymentGateway {
String charge(String customerId, BigDecimal amount);
}
class CardGateway implements PaymentGateway {
public String charge(String customerId, BigDecimal amount) {
return "card-" + customerId;
}
}
class InvoiceGateway implements PaymentGateway {
public String charge(String customerId, BigDecimal amount) {
return "invoice-" + customerId;
}
}
public class CheckoutService {
private final PaymentGateway gateway;
// Two candidates exist; @Primary makes CardGateway the one injected here.
public CheckoutService(PaymentGateway gateway) {
this.gateway = gateway;
}
public String pay(String customerId, BigDecimal amount) {
return gateway.charge(customerId, amount);
}
}When to use it
- Keeping card payments as the default while invoice payments are requested explicitly
- Adding a bounded executor for reports without changing every class that injects ExecutorService
- Shipping a replaceable default implementation in a shared module with @Fallback
Frequently asked questions
What is the difference between @Primary and @Qualifier?
@Primary is set on the bean and makes it the default everywhere. @Qualifier is set at the injection point and picks a specific bean there. A @Qualifier always wins over @Primary.
What happens with two @Primary beans of the same type?
Injection is ambiguous again and startup fails with NoUniqueBeanDefinitionException. Only one candidate per type should be primary.
More examples
@Primary on a @Bean method
Plain ExecutorService injection gets the virtual-thread executor. The report job asks for the bounded pool explicitly with @Qualifier("reportExecutor").
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration(proxyBeanMethods = false)
public class ExecutorsConfig {
@Bean
@Primary
ExecutorService taskExecutor() {
return Executors.newVirtualThreadPerTaskExecutor();
}
@Bean
ExecutorService reportExecutor() {
return Executors.newFixedThreadPool(2);
}
}@Fallback for a replaceable default
If another module declares its own IdGenerator, that one wins and this bean is passed over, with no ambiguity and no need to edit this class.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Fallback;
interface IdGenerator {
String next();
}
@Configuration(proxyBeanMethods = false)
public class IdConfig {
// Used only if the application defines no other IdGenerator.
@Bean
@Fallback
IdGenerator randomIds() {
return () -> java.util.UUID.randomUUID().toString();
}
}Testing which bean wins
getBean(Greeter.class) resolves to the primary bean, while getBeansOfType still returns both, which mirrors how single and collection injection behave.
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
interface Greeter {
String greet();
}
@Configuration(proxyBeanMethods = false)
class GreeterConfig {
@Bean
@Primary
Greeter formal() {
return () -> "Good morning";
}
@Bean
Greeter casual() {
return () -> "Hi";
}
}
class PrimaryTest {
@Test
void primaryBeanIsInjectedByType() {
new ApplicationContextRunner()
.withUserConfiguration(GreeterConfig.class)
.run(context -> {
assertThat(context.getBean(Greeter.class).greet()).isEqualTo("Good morning");
assertThat(context.getBeansOfType(Greeter.class)).hasSize(2);
});
}
}
Discussion