Spring Boot
Qualifier Annotation
The @Qualifier annotation resolves ambiguity when Spring finds multiple beans of the same type. When you have two implementations of an interface, @Qualifier tells Spring exactly which bean to inject. Combined with @Primary for default selection, @Qualifier gives precise control over which bean goes where.
The Problem @Qualifier Solves
When Spring finds exactly one bean of the required type, injection works automatically. When Spring finds multiple beans of the same type, it throws NoUniqueBeanDefinitionException — it cannot decide which to inject. @Qualifier resolves this ambiguity by specifying the exact bean name to inject.
This situation is common in real applications: multiple DataSource beans for read/write splitting, multiple MessageConverter implementations, multiple PaymentService implementations for different providers, multiple cache managers for different caching strategies. Without a disambiguation mechanism, you cannot inject specific implementations — only the default one.
Basic @Qualifier Usage
@Qualifier specifies the bean name to inject when multiple beans of the same type exist. The qualifier value matches the bean name — which defaults to the class name in camelCase when using component scanning.
Java
// Two implementations of the same interface:
@Component
public class StripePaymentService implements PaymentService {
public PaymentResult charge(String customerId, BigDecimal amount) {
System.out.println("Charging via Stripe");
return new PaymentResult("stripe-txn-" + UUID.randomUUID());
}
}
@Component
public class PaypalPaymentService implements PaymentService {
public PaymentResult charge(String customerId, BigDecimal amount) {
System.out.println("Charging via PayPal");
return new PaymentResult("paypal-txn-" + UUID.randomUUID());
}
}
// Without @Qualifier — Spring throws NoUniqueBeanDefinitionException:
@Service
public class OrderService {
@Autowired
private PaymentService paymentService; // FAILS — two matching beans
}
// With @Qualifier — specify exactly which bean:
@Service
public class OrderService {
private final PaymentService paymentService;
// Constructor injection with @Qualifier:
public OrderService(@Qualifier("stripePaymentService") PaymentService paymentService) {
this.paymentService = paymentService;
}
}
// Default bean names from component scanning:
// StripePaymentService → "stripePaymentService" (class name, first letter lowercase)
// PaypalPaymentService → "paypalPaymentService"
// Explicitly named beans:
@Component("stripe")
public class StripePaymentService implements PaymentService { }
@Component("paypal")
public class PaypalPaymentService implements PaymentService { }
// Inject by explicit name:
public OrderService(@Qualifier("stripe") PaymentService paymentService) {
this.paymentService = paymentService;
}
// @Qualifier in @Bean methods:
@Configuration
public class PaymentConfig {
@Bean("stripeGateway")
public PaymentService stripePaymentService() {
return new StripePaymentService(stripeApiKey);
}
@Bean("paypalGateway")
public PaymentService paypalPaymentService() {
return new PaypalPaymentService(paypalClientId);
}
}
// Inject specific @Bean:
public OrderService(@Qualifier("stripeGateway") PaymentService paymentService) {
this.paymentService = paymentService;
}@Primary — The Default Bean
@Primary marks one bean as the default candidate when multiple beans of the same type exist. When @Qualifier is not specified, Spring injects the @Primary bean. This is cleaner than adding @Qualifier everywhere when one implementation is used in the vast majority of cases.
Java
// @Primary — default implementation used unless @Qualifier specifies otherwise:
@Component
@Primary // default PaymentService — used when no @Qualifier is specified
public class StripePaymentService implements PaymentService {
public PaymentResult charge(String customerId, BigDecimal amount) {
return stripeClient.charge(customerId, amount);
}
}
@Component
public class PaypalPaymentService implements PaymentService {
public PaymentResult charge(String customerId, BigDecimal amount) {
return paypalClient.charge(customerId, amount);
}
}
// Injection without @Qualifier — gets the @Primary bean (Stripe):
@Service
public class StandardOrderService {
private final PaymentService paymentService; // gets StripePaymentService
public StandardOrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
// Injection with @Qualifier — gets the specific bean regardless of @Primary:
@Service
public class PaypalOrderService {
private final PaymentService paymentService; // gets PaypalPaymentService
public PaypalOrderService(@Qualifier("paypalPaymentService") PaymentService paymentService) {
this.paymentService = paymentService;
}
}
// @Primary in @Bean methods — same behavior:
@Configuration
public class DataSourceConfig {
@Bean
@Primary // default DataSource — used for most operations
public DataSource primaryDataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://primary-db:3306/mydb")
.build();
}
@Bean("readOnlyDataSource")
public DataSource readOnlyDataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://replica-db:3306/mydb")
.build();
}
}
// Read/write split — most services get the primary DataSource:
@Repository
public class UserRepository {
private final JdbcTemplate jdbcTemplate; // uses primaryDataSource automatically
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}
// Read-heavy service gets the replica explicitly:
@Service
public class ReportService {
private final JdbcTemplate readJdbc;
public ReportService(@Qualifier("readOnlyDataSource") DataSource dataSource) {
this.readJdbc = new JdbcTemplate(dataSource);
}
}Custom Qualifier Annotations
Custom qualifier annotations are cleaner than string-based @Qualifier. Instead of @Qualifier("stripePaymentService") — a string that can be misspelled — you create a dedicated annotation that is type-safe and self-documenting.
Java
// Define custom qualifier annotations:
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier // meta-annotated with @Qualifier — makes this a qualifier
public @interface StripePayment { }
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface PaypalPayment { }
// Apply to bean definitions:
@Component
@StripePayment // custom qualifier on the bean
public class StripePaymentService implements PaymentService {
public PaymentResult charge(String customerId, BigDecimal amount) {
return stripeClient.charge(customerId, amount);
}
}
@Component
@PaypalPayment // custom qualifier on the bean
public class PaypalPaymentService implements PaymentService {
public PaymentResult charge(String customerId, BigDecimal amount) {
return paypalClient.charge(customerId, amount);
}
}
// Inject with custom qualifier:
@Service
public class CheckoutService {
private final PaymentService stripeService;
private final PaymentService paypalService;
public CheckoutService(
@StripePayment PaymentService stripeService, // type-safe — no strings
@PaypalPayment PaymentService paypalService) { // IDE completes this
this.stripeService = stripeService;
this.paypalService = paypalService;
}
public PaymentResult processPayment(Order order, PaymentMethod method) {
return switch (method) {
case STRIPE -> stripeService.charge(order.getCustomerId(), order.getTotal());
case PAYPAL -> paypalService.charge(order.getCustomerId(), order.getTotal());
};
}
}
// Benefits of custom qualifiers over string-based @Qualifier:
// 1. TYPE SAFE — @StripePayment cannot be misspelled the way "stripePaymentService" can
// 2. REFACTORING SAFE — rename the annotation and the IDE updates all usages
// 3. SELF-DOCUMENTING — @StripePayment communicates intent better than a string
// 4. SEARCHABLE — find all usages of @StripePayment with IDE navigation
// 5. COMPOSABLE — can carry additional metadata as annotation attributes
// Custom qualifier with attributes — for parameterized selection:
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface PaymentProvider {
String value(); // "stripe", "paypal", "braintree"
}
@Component
@PaymentProvider("stripe")
public class StripeService implements PaymentService { }
@Component
@PaymentProvider("paypal")
public class PaypalService implements PaymentService { }
// Inject:
public OrderService(@PaymentProvider("stripe") PaymentService service) { }@Qualifier with Collections
When injecting a collection of all beans of a type, @Qualifier filters which beans are included. This enables targeted collection injection without getting every implementation.
Java
// Inject ALL beans of a type — unfiltered:
@Service
public class PaymentRouter {
private final List<PaymentService> allPaymentServices;
public PaymentRouter(List<PaymentService> allPaymentServices) {
this.allPaymentServices = allPaymentServices;
// Contains: StripePaymentService, PaypalPaymentService, BraintreePaymentService
}
public PaymentResult route(Order order, String providerName) {
return allPaymentServices.stream()
.filter(s -> s.getProviderName().equals(providerName))
.findFirst()
.orElseThrow(() -> new UnsupportedPaymentProviderException(providerName))
.charge(order.getCustomerId(), order.getTotal());
}
}
// Inject as a Map — bean name is the key:
@Service
public class PaymentDispatcher {
private final Map<String, PaymentService> serviceMap;
public PaymentDispatcher(Map<String, PaymentService> serviceMap) {
this.serviceMap = serviceMap;
// {"stripePaymentService": ..., "paypalPaymentService": ..., "braintreePaymentService": ...}
}
public PaymentResult dispatch(String beanName, Order order) {
PaymentService service = serviceMap.get(beanName);
if (service == null) throw new IllegalArgumentException("Unknown service: " + beanName);
return service.charge(order.getCustomerId(), order.getTotal());
}
}
// Use @Order to control the order in a List:
@Component
@Order(1) // first in the list
public class StripePaymentService implements PaymentService { }
@Component
@Order(2) // second in the list
public class PaypalPaymentService implements PaymentService { }
@Component
@Order(3) // third in the list
public class BraintreePaymentService implements PaymentService { }
// Filter collection with custom qualifier:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface EuropeanProvider { }
@Component
@EuropeanProvider
@Order(1)
public class StripeEUService implements PaymentService { }
@Component
@EuropeanProvider
@Order(2)
public class AdyenService implements PaymentService { }
@Component // US-only — not @EuropeanProvider
public class StripeUSService implements PaymentService { }
// Inject only European providers:
@Service
public class EuropeanCheckoutService {
private final List<PaymentService> europeanServices;
public EuropeanCheckoutService(
@EuropeanProvider List<PaymentService> europeanServices) {
this.europeanServices = europeanServices;
// Contains only: StripeEUService, AdyenService
// StripeUSService is excluded
}
}@Qualifier in Common Real-World Scenarios
These are the scenarios where @Qualifier appears most frequently in production Spring Boot applications — datasource routing, cache managers, executor services, and message converters.
Java
// ── SCENARIO 1: Read/Write DataSource Routing ─────────────────────
@Configuration
public class DataSourceConfig {
@Bean
@Primary
public DataSource writeDataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://primary:3306/mydb")
.build();
}
@Bean("readDataSource")
public DataSource readDataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://replica:3306/mydb")
.build();
}
}
@Repository
public class UserWriteRepository {
private final JdbcTemplate jdbc; // gets writeDataSource (primary)
public UserWriteRepository(JdbcTemplate jdbc) { this.jdbc = jdbc; }
public User save(User user) { return null; }
}
@Repository
public class UserReadRepository {
private final JdbcTemplate jdbc; // gets readDataSource explicitly
public UserReadRepository(@Qualifier("readDataSource") DataSource ds) {
this.jdbc = new JdbcTemplate(ds);
}
public List<User> findAll() { return null; }
}
// ── SCENARIO 2: Multiple Cache Managers ───────────────────────────
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
@Primary
public CacheManager redisCacheManager(RedisConnectionFactory factory) {
return RedisCacheManager.builder(factory).build();
}
@Bean("localCacheManager")
public CacheManager caffeineCacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder().maximumSize(1000));
return manager;
}
}
@Service
public class ProductService {
// Most caching uses Redis (primary):
@Cacheable(value = "products", cacheManager = "redisCacheManager")
public Product findById(Long id) { return null; }
// Hot data cached locally for ultra-fast access:
@Cacheable(value = "hotProducts", cacheManager = "localCacheManager")
public List<Product> findTopSellers() { return null; }
}
// ── SCENARIO 3: Multiple TaskExecutors ───────────────────────────
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("ioExecutor")
public Executor ioTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20); // many threads for IO-bound work
executor.setMaxPoolSize(50);
executor.setThreadNamePrefix("io-");
executor.initialize();
return executor;
}
@Bean("cpuExecutor")
public Executor cpuTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
executor.setThreadNamePrefix("cpu-");
executor.initialize();
return executor;
}
}
@Service
public class ProcessingService {
// IO-bound — use the IO executor:
@Async("ioExecutor")
public CompletableFuture<String> fetchFromApi(String url) {
return CompletableFuture.completedFuture(httpClient.get(url));
}
// CPU-bound — use the CPU executor:
@Async("cpuExecutor")
public CompletableFuture<byte[]> generateReport(ReportRequest request) {
return CompletableFuture.completedFuture(reportEngine.generate(request));
}
}@Qualifier vs @Primary vs Bean Name Matching
Spring has three mechanisms for resolving ambiguity when multiple beans match. Understanding their precedence and appropriate use prevents over-engineering and inconsistent patterns.
Java
// THREE DISAMBIGUATION MECHANISMS — precedence order:
// MECHANISM 1 — Field/parameter name matching (lowest precedence, implicit):
@Service
public class OrderService {
private final PaymentService stripePaymentService; // name matches bean name!
public OrderService(PaymentService stripePaymentService) {
this.stripePaymentService = stripePaymentService;
// Spring matches "stripePaymentService" parameter name to the bean name
// Works — but fragile! Renaming the parameter breaks injection silently
}
}
// AVOID: implicit name matching is not obvious and breaks on renaming
// MECHANISM 2 — @Primary (medium precedence, explicit default):
@Component
@Primary
public class StripePaymentService implements PaymentService { }
// Default bean — used when no qualifier is specified
// USE WHEN: one implementation is the standard choice in 80%+ of cases
// MECHANISM 3 — @Qualifier (highest precedence, explicit override):
@Service
public class SpecialOrderService {
private final PaymentService paymentService;
public SpecialOrderService(@Qualifier("paypalPaymentService") PaymentService service) {
this.paymentService = service;
}
}
// Explicit override — ignores @Primary, uses exactly the specified bean
// USE WHEN: a specific injection point needs a non-default bean
// DECISION GUIDE:
// One implementation → no disambiguation needed
// Multiple, one is the default → @Primary on the default, @Qualifier for others
// Multiple, no clear default → @Qualifier everywhere (or custom qualifiers)
// Type-safe qualification → custom @Qualifier annotations
// ANTI-PATTERN — string-based @Qualifier everywhere:
// @Qualifier("stripePaymentService") — brittle string, easy to misspell
// Better: @StripePayment (custom annotation) — type-safe, refactoring-safe
// ANTI-PATTERN — @Primary and @Qualifier everywhere:
// If every injection point has @Qualifier, @Primary is irrelevant
// If no injection point has @Qualifier, only one @Primary is needed
// Design your disambiguation strategy consistently across the codebase