Spring BootPrimary Annotation
Spring Boot

Primary Annotation

The @Primary annotation marks one bean as the default candidate when multiple beans of the same type exist in the Spring container. When Spring encounters ambiguity during injection, it selects the @Primary bean automatically — no @Qualifier needed at the injection point. Understanding @Primary, when to use it, and how it interacts with @Qualifier and auto-configuration is essential for managing multiple implementations cleanly.

What Is @Primary?

When the Spring container finds multiple beans of the same type and needs to inject one, it throws NoUniqueBeanDefinitionException — it cannot decide which bean to use. @Primary resolves this by marking one bean as the preferred candidate. Any injection point that doesn't specify a @Qualifier receives the @Primary bean automatically. @Primary expresses a default — not an exclusive choice. Injection points that need a specific non-primary bean can still use @Qualifier to override the default. The combination of @Primary for the common case and @Qualifier for the exception case is the standard disambiguation pattern in Spring. @Primary is most appropriate when: - One implementation is genuinely the standard choice used in the vast majority of injection points - You want to provide a sensible default while still allowing specific overrides - You're overriding a Spring Boot auto-configured bean with your own implementation - You're writing a library or starter that provides a default implementation others can override

Basic @Primary Usage

Apply @Primary to one bean definition when multiple beans of the same type exist. The annotated bean becomes the default and is injected wherever the type is requested without a @Qualifier.
Java
// Two implementations of PaymentService:
@Component
@Primary   // this is the default — injected when no @Qualifier is specified
public class StripePaymentService implements PaymentService {

    private final String apiKey;

    public StripePaymentService(@Value("${stripe.api.key}") String apiKey) {
        this.apiKey = apiKey;
    }

    @Override
    public PaymentResult charge(String customerId, BigDecimal amount) {
        // real Stripe implementation
        return new PaymentResult("stripe-txn-" + UUID.randomUUID(), PaymentStatus.SUCCESS);
    }

    @Override
    public String getProviderName() { return "stripe"; }
}

@Component
public class PaypalPaymentService implements PaymentService {

    @Override
    public PaymentResult charge(String customerId, BigDecimal amount) {
        // real PayPal implementation
        return new PaymentResult("paypal-txn-" + UUID.randomUUID(), PaymentStatus.SUCCESS);
    }

    @Override
    public String getProviderName() { return "paypal"; }
}

// Injection without @Qualifier — gets @Primary bean (Stripe):
@Service
public class StandardCheckoutService {

    private final PaymentService paymentService;  // receives StripePaymentService

    public StandardCheckoutService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public Order checkout(Cart cart) {
        PaymentResult result = paymentService.charge(cart.getCustomerId(), cart.getTotal());
        return new Order(cart, result);
    }
}

// Injection with @Qualifier — overrides @Primary:
@Service
public class PaypalCheckoutService {

    private final PaymentService paymentService;  // receives PaypalPaymentService

    public PaypalCheckoutService(
            @Qualifier("paypalPaymentService") PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}

// The rule: @Qualifier always wins over @Primary
// @Primary is the default, @Qualifier is the explicit override

@Primary in @Bean Methods

@Primary works identically on @Bean methods in @Configuration classes. This is the most common placement when configuring third-party infrastructure beans like DataSource, ObjectMapper, or RestTemplate.
Java
@Configuration
public class DataSourceConfig {

    // Primary — used by Spring Boot's auto-configured JdbcTemplate, JPA, etc.:
    @Bean
    @Primary
    public DataSource primaryDataSource(DataSourceProperties props) {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(props.getUrl());
        config.setUsername(props.getUsername());
        config.setPassword(props.getPassword());
        config.setMaximumPoolSize(20);
        config.setPoolName("primary-pool");
        return new HikariDataSource(config);
    }

    // Read replica — injected only when explicitly qualified:
    @Bean("readOnlyDataSource")
    public DataSource readOnlyDataSource(DataSourceProperties props) {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(props.getUrl().replace("primary", "replica"));
        config.setUsername(props.getUsername());
        config.setPassword(props.getPassword());
        config.setMaximumPoolSize(50);   // more connections — read-heavy
        config.setReadOnly(true);
        config.setPoolName("readonly-pool");
        return new HikariDataSource(config);
    }

    // Reporting database — separate schema entirely:
    @Bean("reportingDataSource")
    public DataSource reportingDataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://reporting-db:3306/reports");
        config.setMaximumPoolSize(5);
        config.setPoolName("reporting-pool");
        return new HikariDataSource(config);
    }
}

// JdbcTemplate and JPA use primaryDataSource automatically:
@Repository
public class UserRepository {
    private final JdbcTemplate jdbcTemplate;  // auto-configured with primaryDataSource
    public UserRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
}

// Read-heavy service uses the replica explicitly:
@Repository
public class ProductSearchRepository {
    private final JdbcTemplate readJdbc;

    public ProductSearchRepository(
            @Qualifier("readOnlyDataSource") DataSource dataSource) {
        this.readJdbc = new JdbcTemplate(dataSource);
    }

    public List<Product> search(String query) {
        return readJdbc.query(
            "SELECT * FROM products WHERE name LIKE ?",
            productRowMapper,
            "%" + query + "%"
        );
    }
}

// Reporting service uses the reporting database:
@Service
public class ReportGenerationService {
    private final JdbcTemplate reportJdbc;

    public ReportGenerationService(
            @Qualifier("reportingDataSource") DataSource dataSource) {
        this.reportJdbc = new JdbcTemplate(dataSource);
    }
}

Overriding Spring Boot Auto-Configuration with @Primary

The most powerful use of @Primary in Spring Boot applications is overriding auto-configured beans. Spring Boot auto-configures beans conditionally — if you define your own bean of the same type, the auto-configuration backs off. @Primary ensures your custom bean is used everywhere, including by other auto-configured beans that depend on it.
Java
// Override Spring Boot's auto-configured ObjectMapper:
@Configuration
public class JacksonConfig {

    @Bean
    @Primary   // replaces Spring Boot's auto-configured ObjectMapper everywhere
    public ObjectMapper objectMapper() {
        return JsonMapper.builder()
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
            .configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true)
            .addModule(new JavaTimeModule())
            .addModule(new Jdk8Module())
            .serializationInclusion(JsonInclude.Include.NON_NULL)
            .build();
    }
}
// Spring Boot's MVC message converters, WebFlux, and everything else
// now uses your customized ObjectMapper automatically

// Override Spring Boot's auto-configured TaskExecutor:
@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean
    @Primary
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("app-async-");
        executor.setRejectedExecutionHandler(
            new ThreadPoolExecutor.CallerRunsPolicy()
        );
        executor.initialize();
        return executor;
    }
}
// All @Async methods now use this executor instead of Spring Boot's default

// Override Spring Boot's auto-configured PasswordEncoder:
@Configuration
public class SecurityConfig {

    @Bean
    @Primary
    public PasswordEncoder passwordEncoder() {
        // Use Argon2 instead of BCrypt for stronger security:
        return new Argon2PasswordEncoder(16, 32, 1, 65536, 10);
    }
}

// Check what Spring Boot auto-configures and what your bean overrides:
// application.properties:
// debug=true
// Look for "UserDefinedBeanCondition" in the conditions report —
// Spring Boot reports when your bean caused an auto-config to back off

@Primary in Testing

@Primary is frequently used in tests to replace production beans with test doubles. @TestConfiguration classes with @Primary beans override the real beans for the duration of the test.
Java
// Production service:
@Service
public class OrderService {
    private final PaymentService paymentService;  // injected — could be any PaymentService
    private final EmailService emailService;

    public OrderService(PaymentService paymentService, EmailService emailService) {
        this.paymentService = paymentService;
        this.emailService = emailService;
    }

    public Order createOrder(CreateOrderRequest request) {
        PaymentResult payment = paymentService.charge(
            request.customerId(), request.totalAmount()
        );
        Order order = orderRepository.save(new Order(request, payment));
        emailService.sendConfirmation(order);
        return order;
    }
}

// Test with @Primary to override production beans:
@SpringBootTest
class OrderServiceIntegrationTest {

    @TestConfiguration
    static class TestOverrides {

        // @Primary overrides the real StripePaymentService:
        @Bean
        @Primary
        public PaymentService mockPaymentService() {
            PaymentService mock = Mockito.mock(PaymentService.class);
            when(mock.charge(anyString(), any(BigDecimal.class)))
                .thenReturn(new PaymentResult("test-txn-001", PaymentStatus.SUCCESS));
            return mock;
        }

        // @Primary overrides the real SmtpEmailService:
        @Bean
        @Primary
        public EmailService noOpEmailService() {
            return (email, subject, body) -> {
                System.out.println("Test email suppressed: " + subject);
            };
        }
    }

    @Autowired
    private OrderService orderService;

    @Autowired
    private PaymentService paymentService;   // gets the mock from TestOverrides

    @Test
    void createOrder_chargesPaymentAndReturnsOrder() {
        CreateOrderRequest request = new CreateOrderRequest(
            "customer-1", new BigDecimal("99.99"), List.of("PROD-1")
        );

        Order order = orderService.createOrder(request);

        assertThat(order).isNotNull();
        assertThat(order.getPaymentTransactionId()).isEqualTo("test-txn-001");
        verify(paymentService).charge("customer-1", new BigDecimal("99.99"));
    }
}

// @MockBean — simpler alternative that replaces beans in the context:
@SpringBootTest
class OrderServiceMockBeanTest {

    @MockBean  // replaces the real PaymentService bean in the context
    private PaymentService paymentService;

    @MockBean
    private EmailService emailService;

    @Autowired
    private OrderService orderService;

    @Test
    void createOrder_withMockBeans() {
        when(paymentService.charge(any(), any()))
            .thenReturn(new PaymentResult("mock-txn", PaymentStatus.SUCCESS));

        Order order = orderService.createOrder(buildRequest());

        assertThat(order).isNotNull();
    }
}
// @MockBean automatically replaces the bean — no @Primary needed
// Use @TestConfiguration + @Primary when you need a real (non-mock) test double

@Primary with Profiles

Combining @Primary with @Profile is a powerful pattern for environment-specific default beans. Different profiles activate different @Primary implementations, giving each environment a tailored default without injection point changes.
Java
// Interface:
public interface StorageService {
    String upload(String path, byte[] data);
    byte[] download(String path);
    void delete(String path);
}

// Development — local file system, marked @Primary for dev profile:
@Component
@Profile("dev")
@Primary
public class LocalFileStorageService implements StorageService {

    private final Path basePath = Path.of("/tmp/dev-uploads");

    @PostConstruct
    public void init() throws IOException {
        Files.createDirectories(basePath);
    }

    @Override
    public String upload(String path, byte[] data) throws IOException {
        Path target = basePath.resolve(path);
        Files.createDirectories(target.getParent());
        Files.write(target, data);
        return "file://" + target.toAbsolutePath();
    }

    @Override
    public byte[] download(String path) throws IOException {
        return Files.readAllBytes(basePath.resolve(path));
    }

    @Override
    public void delete(String path) throws IOException {
        Files.deleteIfExists(basePath.resolve(path));
    }
}

// Test — in-memory, @Primary for test profile:
@Component
@Profile("test")
@Primary
public class InMemoryStorageService implements StorageService {

    private final Map<String, byte[]> store = new ConcurrentHashMap<>();

    @Override
    public String upload(String path, byte[] data) {
        store.put(path, data);
        return "memory://" + path;
    }

    @Override
    public byte[] download(String path) {
        byte[] data = store.get(path);
        if (data == null) throw new FileNotFoundException("Not found: " + path);
        return data;
    }

    @Override
    public void delete(String path) {
        store.remove(path);
    }
}

// Production — AWS S3, @Primary for prod profile:
@Component
@Profile("prod")
@Primary
public class S3StorageService implements StorageService {

    private final AmazonS3 s3Client;
    private final String bucketName;

    public S3StorageService(AmazonS3 s3Client,
                             @Value("${aws.s3.bucket}") String bucketName) {
        this.s3Client = s3Client;
        this.bucketName = bucketName;
    }

    @Override
    public String upload(String path, byte[] data) {
        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentLength(data.length);
        s3Client.putObject(bucketName, path,
            new ByteArrayInputStream(data), metadata);
        return "s3://" + bucketName + "/" + path;
    }

    @Override
    public byte[] download(String path) {
        S3Object obj = s3Client.getObject(bucketName, path);
        return obj.getObjectContent().readAllBytes();
    }

    @Override
    public void delete(String path) {
        s3Client.deleteObject(bucketName, path);
    }
}

// All services inject StorageService without qualification:
@Service
@RequiredArgsConstructor
public class UserAvatarService {
    private final StorageService storageService;  // gets the right impl per profile

    public String uploadAvatar(Long userId, byte[] imageData) {
        return storageService.upload("avatars/" + userId + ".jpg", imageData);
    }
}
// dev profile  → LocalFileStorageService (fast, no cloud setup)
// test profile → InMemoryStorageService (fast, isolated, no disk)
// prod profile → S3StorageService (durable, scalable)
// Zero changes to UserAvatarService or any other consumer

Multiple @Primary Beans — The Error

Only one bean of a given type can be @Primary. Declaring two @Primary beans of the same type causes NoUniqueBeanDefinitionException at startup — the same error you were trying to avoid.
Java
// WRONG — two @Primary beans of the same type:
@Component
@Primary   // first @Primary
public class StripePaymentService implements PaymentService { }

@Component
@Primary   // second @Primary — CONFLICT
public class PaypalPaymentService implements PaymentService { }

// Spring throws at startup:
// NoUniqueBeanDefinitionException: expected single matching bean but found 2:
// stripePaymentService, paypalPaymentService
// (both are @Primary — Spring still cannot decide)

// FIX — only one @Primary per type:
@Component
@Primary   // Stripe is the default
public class StripePaymentService implements PaymentService { }

@Component  // PayPal is the non-default
public class PaypalPaymentService implements PaymentService { }

// FIX — if genuinely no default exists, use @Qualifier everywhere:
// No @Primary on either bean
// All injection points use @Qualifier explicitly:
@Service
public class CheckoutService {
    private final PaymentService stripeService;
    private final PaymentService paypalService;

    public CheckoutService(
            @Qualifier("stripePaymentService") PaymentService stripeService,
            @Qualifier("paypalPaymentService") PaymentService paypalService) {
        this.stripeService = stripeService;
        this.paypalService = paypalService;
    }
}

// FIX — inject all implementations as a collection:
@Service
public class PaymentRouter {
    private final Map<String, PaymentService> services;

    public PaymentRouter(Map<String, PaymentService> services) {
        this.services = services;
        // {"stripePaymentService": ..., "paypalPaymentService": ...}
    }

    public PaymentResult route(String provider, Order order) {
        PaymentService service = services.get(provider + "PaymentService");
        if (service == null) throw new UnsupportedPaymentProviderException(provider);
        return service.charge(order.getCustomerId(), order.getTotal());
    }
}

@Primary vs @Qualifier — Complete Decision Guide

Choosing between @Primary and @Qualifier comes down to understanding the relationship between the default case and the exception case in your application.
Java
// ── SCENARIO 1: One implementation is clearly the standard ──────────
// → Use @Primary on the standard, @Qualifier at the exception injection points

@Component @Primary
public class StripePaymentService implements PaymentService { }  // standard

@Component
public class PaypalPaymentService implements PaymentService { }  // exception

@Service
public class CheckoutService {
    // 90% of services just need "the payment service" → gets Stripe automatically:
    public CheckoutService(PaymentService paymentService) { }
}

@Service
public class PaypalCheckoutService {
    // The one service that needs PayPal specifically:
    public PaypalCheckoutService(@Qualifier("paypalPaymentService") PaymentService ps) { }
}


// ── SCENARIO 2: No clear default — all equal alternatives ──────────
// → No @Primary anywhere, use @Qualifier at every injection point
// → Or inject as a collection and dispatch at runtime

@Component public class StripePaymentService implements PaymentService { }
@Component public class PaypalPaymentService implements PaymentService { }
@Component public class BraintreePaymentService implements PaymentService { }

// Inject all and dispatch dynamically:
@Service
public class SmartPaymentRouter {
    private final Map<String, PaymentService> services;
    public SmartPaymentRouter(Map<String, PaymentService> services) {
        this.services = services;
    }
    public PaymentResult pay(String provider, Order order) {
        return services.get(provider + "PaymentService")
                       .charge(order.getCustomerId(), order.getTotal());
    }
}


// ── SCENARIO 3: Overriding auto-configuration ───────────────────────
// → Use @Primary to ensure your bean takes precedence over auto-configured one

@Bean
@Primary   // overrides Spring Boot's auto-configured ObjectMapper
public ObjectMapper objectMapper() {
    return JsonMapper.builder()
        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
        .build();
}


// ── SCENARIO 4: Environment-specific defaults ───────────────────────
// → Use @Primary + @Profile on each implementation

@Component @Profile("dev")  @Primary public class DevEmailService  implements EmailService { }
@Component @Profile("test") @Primary public class TestEmailService implements EmailService { }
@Component @Profile("prod") @Primary public class SmtpEmailService implements EmailService { }


// ── SUMMARY TABLE ───────────────────────────────────────────────────
// Situation                            Solution
// ─────────────────────────────────────────────────────────────────────────
// One impl is the default (80%+ usage)  @Primary on default, @Qualifier for exceptions
// No clear default                      @Qualifier everywhere, or inject as collection
// Override auto-configuration           @Primary on your bean
// Environment-specific defaults         @Primary + @Profile
// Multiple beans needed in one class    Both @Qualifier on each parameter
// Type-safe disambiguation              Custom @Qualifier annotations