@Primary

Marks one bean as the default choice when several beans match an injection point, so plain injection by type is no longer ambiguous.

Signature
@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 DataSource and 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 @Primary beans of one type. Each was added to fix a different conflict, and together they bring the ambiguity back.
  • Using @Primary on 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; @Primary only 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

Example · java
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);
}

@Component
@Primary
class CardGateway implements PaymentGateway {
    public String charge(String customerId, BigDecimal amount) {
        return "card-" + customerId;
    }
}

@Component
class InvoiceGateway implements PaymentGateway {
    public String charge(String customerId, BigDecimal amount) {
        return "invoice-" + customerId;
    }
}

@Service
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").

Example · java
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.

Example · java
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.

Example · java
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

  • Be the first to comment on this lesson.