Spring Framework & the IoC Container

Bean Naming, Qualifiers & Multiple Beans

18 min Lesson 8 of 13

Bean Naming, Qualifiers & Multiple Beans

Spring lets you register as many beans of the same type as you like — and that is a feature, not a bug. You might have a DataSource bean for your primary database and another for a read replica, two RestTemplate beans configured with different timeouts, or several implementations of a PaymentGateway interface. The challenge shifts from how do I register a bean? to how does Spring know which bean to inject when there are several candidates? This lesson answers that question completely.

How Spring Names Beans by Default

Every bean has at least one name. When you declare a bean Spring derives a default name from the source:

  • @Component (and stereotypes): the simple class name with its first letter lowercased. OrderService becomes "orderService".
  • @Bean method: the method name. A method called primaryDataSource() registers a bean named "primaryDataSource".
  • XML <bean>: the id attribute, falling back to a generated name if omitted.

You can override the default name anywhere a name is derived:

// Override on a component @Component("orderSvc") public class OrderService { ... } // Override on a @Bean method — first value is the primary name, // remaining values are aliases @Bean({"primaryDs", "mainDatabase"}) public DataSource primaryDataSource() { ... }
Names are global within an ApplicationContext. If two beans share the same name the second registration silently overwrites the first (in older Spring versions) or throws a BeanDefinitionOverrideException (Spring Boot 2.1+ by default). Always choose unique, descriptive names.

Injecting by Name with @Qualifier

When Spring resolves a dependency by type and finds more than one matching bean it throws NoUniqueBeanDefinitionException. The standard fix is @Qualifier, which tells Spring exactly which bean name to use:

@Configuration public class DataSourceConfig { @Bean("primaryDs") public DataSource primaryDataSource() { // returns connection pool pointing at the write master return buildPool("jdbc:postgresql://master:5432/shop"); } @Bean("replicaDs") public DataSource replicaDataSource() { // returns connection pool pointing at the read replica return buildPool("jdbc:postgresql://replica:5432/shop"); } private DataSource buildPool(String url) { /* HikariCP setup */ return null; } } @Service public class ReportService { private final DataSource dataSource; public ReportService(@Qualifier("replicaDs") DataSource dataSource) { this.dataSource = dataSource; } }

@Qualifier works on constructor parameters, setter parameters, and field injections alike. The string value must exactly match a registered bean name or alias.

@Primary — Choosing a Default Winner

If one bean should be the default choice for the vast majority of injection points, annotate it with @Primary. Any injection point that does not carry a @Qualifier will receive the primary bean; injection points that do carry a @Qualifier still get exactly what they ask for.

@Configuration public class DataSourceConfig { @Bean @Primary public DataSource primaryDataSource() { ... } // wins unqualified injections @Bean("replicaDs") public DataSource replicaDataSource() { ... } // only injected when explicitly qualified } @Service public class OrderService { // No @Qualifier — receives primaryDataSource automatically public OrderService(DataSource dataSource) { ... } }
Use @Primary to declare intent, not to avoid naming things. Adding @Primary without also giving your beans clear names is a recipe for confusion when a third bean is added later. Always name beans explicitly and use @Primary as the tiebreaker.

Custom Qualifier Annotations

Scattering string literals like "replicaDs" across dozens of injection points is fragile — a typo compiles fine but fails at runtime. The professional approach is a custom qualifier annotation:

import org.springframework.beans.factory.annotation.Qualifier; import java.lang.annotation.*; @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Qualifier // meta-annotated with @Qualifier public @interface ReadReplica { } // your custom marker

Apply it at the bean definition and at the injection site:

@Bean @ReadReplica public DataSource replicaDataSource() { ... } @Service public class ReportService { public ReportService(@ReadReplica DataSource dataSource) { ... } }

Now refactoring the qualifier is a compile-time operation — rename the annotation and your IDE updates all usages. String typos are impossible.

Injecting All Beans of a Type

Sometimes you want every registered implementation, not just one. Spring will happily inject a List<T> or Map<String, T> containing all beans of type T:

public interface NotificationChannel { void send(String message); } @Component public class EmailChannel implements NotificationChannel { ... } @Component public class SmsChannel implements NotificationChannel { ... } @Component public class SlackChannel implements NotificationChannel { ... } @Service public class NotificationService { // Spring injects all three implementations private final List<NotificationChannel> channels; public NotificationService(List<NotificationChannel> channels) { this.channels = channels; } public void broadcast(String message) { channels.forEach(ch -> ch.send(message)); } }

When injecting a Map<String, T> the keys are the bean names, giving you runtime lookup by name without coupling to Spring APIs in your business logic:

@Service public class ChannelRouter { private final Map<String, NotificationChannel> channelMap; public ChannelRouter(Map<String, NotificationChannel> channelMap) { this.channelMap = channelMap; // channelMap = {"emailChannel": ..., "smsChannel": ..., "slackChannel": ...} } public void send(String channelName, String message) { NotificationChannel ch = channelMap.get(channelName); if (ch == null) throw new IllegalArgumentException("Unknown channel: " + channelName); ch.send(message); } }

Controlling Order in Collections — @Order and Ordered

When Spring populates a List<T>, the order of elements is not guaranteed unless you specify it. Use @Order on the bean class (or implement org.springframework.core.Ordered) to set a priority. Lower values come first:

@Component @Order(1) public class SmsChannel implements NotificationChannel { ... } @Component @Order(2) public class EmailChannel implements NotificationChannel { ... } @Component @Order(3) public class SlackChannel implements NotificationChannel { ... }
@Order does not affect which bean wins an injection — only list ordering. It has no effect on @Qualifier resolution or @Primary elections. Misusing it to try to "override" primary candidates is a common mistake that leads to subtle bugs.

Resolving Beans Programmatically

In rare cases — dynamic dispatch, plugin architectures — you need to look up a bean by name or type at runtime. Inject ApplicationContext and call getBean():

@Service public class DynamicChannelService { private final ApplicationContext ctx; public DynamicChannelService(ApplicationContext ctx) { this.ctx = ctx; } public void send(String beanName, String message) { NotificationChannel ch = ctx.getBean(beanName, NotificationChannel.class); ch.send(message); } }

This pattern is called the Service Locator. It is acceptable at the edges of an architecture (e.g., when the bean name comes from a database row or a user request), but using it in the middle of business logic tightly couples your code to Spring and makes unit testing harder. Prefer collection injection whenever all variants are known at startup.

Decision Guide

  • One clear default, occasional exceptions: use @Primary + @Qualifier at the few exception sites.
  • Two or more equally important beans: give each a clear name or custom qualifier annotation; no @Primary.
  • Fan-out to all implementations: inject List<T> or Map<String, T>.
  • Runtime dynamic dispatch: inject Map<String, T> or fall back to ApplicationContext.getBean().
  • Type-safe refactoring at scale: replace string qualifiers with custom qualifier annotations.

Summary

Spring derives bean names automatically but lets you override them everywhere. When multiple beans of the same type exist, @Qualifier pins an injection to a specific name, and @Primary elects a default winner for unqualified sites. Custom qualifier annotations lift the disambiguation out of fragile strings and into the type system. For fan-out scenarios, injecting a List<T> or Map<String, T> is the cleanest approach, with @Order controlling list ordering. Together these tools let you manage any number of bean candidates without ambiguity and without coupling your business logic to Spring internals.