Spring Boot
Autowiring
Interfaces
Dependency Injection
Java

Spring boot autowiring an interface with multiple implementations

Object-Oriented Design practice on Codemia

Turn requirements into classes, and defend the design, on the problems that come up in OOD rounds.

Practice OOD

Introduction

Autowiring an interface in Spring Boot works automatically only when exactly one matching bean exists. Once you add multiple implementations, injection by type becomes ambiguous and Spring refuses to guess. The correct fix is to make the selection explicit, either at wiring time or by injecting all candidates and choosing deliberately.

Why Spring Throws an Ambiguity Error

Suppose you have one interface and two concrete services:

java
public interface PaymentService {
    String charge(int amount);
}
java
1import org.springframework.stereotype.Service;
2
3@Service
4public class CardPaymentService implements PaymentService {
5    @Override
6    public String charge(int amount) {
7        return "card:" + amount;
8    }
9}
java
1import org.springframework.stereotype.Service;
2
3@Service
4public class PaypalPaymentService implements PaymentService {
5    @Override
6    public String charge(int amount) {
7        return "paypal:" + amount;
8    }
9}

If another bean asks for PaymentService through constructor injection, Spring sees two candidates and throws NoUniqueBeanDefinitionException. That is expected behavior. By type alone, both beans are valid.

Use @Qualifier for an Explicit Choice

@Qualifier is the most direct solution when one class needs one specific implementation.

java
1import org.springframework.beans.factory.annotation.Qualifier;
2import org.springframework.stereotype.Service;
3
4@Service
5public class CheckoutService {
6    private final PaymentService paymentService;
7
8    public CheckoutService(@Qualifier("cardPaymentService") PaymentService paymentService) {
9        this.paymentService = paymentService;
10    }
11
12    public String checkout(int amount) {
13        return paymentService.charge(amount);
14    }
15}

This works best when the dependency is stable and business logic clearly depends on one named implementation. If the bean name matters to the application design, make it explicit rather than relying on the default class-name-derived bean id.

Use @Primary for the Default Implementation

If one implementation should win in most cases, annotate it with @Primary.

java
1import org.springframework.context.annotation.Primary;
2import org.springframework.stereotype.Service;
3
4@Primary
5@Service
6public class CardPaymentService implements PaymentService {
7    @Override
8    public String charge(int amount) {
9        return "card:" + amount;
10    }
11}

Now plain PaymentService injection resolves to that bean unless a @Qualifier overrides it.

@Primary is a good fit when there is a natural default and a few special alternatives. It is a bad fit when the correct implementation depends on request content, tenant, or feature flags.

Inject a Collection for Strategy Selection

Sometimes the application genuinely needs multiple implementations at runtime. In that case, inject a List or Map of beans.

java
1import java.util.Map;
2import org.springframework.stereotype.Component;
3
4@Component
5public class PaymentRegistry {
6    private final Map<String, PaymentService> services;
7
8    public PaymentRegistry(Map<String, PaymentService> services) {
9        this.services = services;
10    }
11
12    public PaymentService resolve(String provider) {
13        return services.get(provider);
14    }
15}

This pattern is useful for strategy selection. For example, a controller might choose a provider based on a request field, while keeping each payment implementation isolated and testable.

Profiles and Conditions Can Also Narrow the Set

If only one implementation should exist in a given environment, Spring profiles or conditional beans can prevent the ambiguity from existing in the first place.

java
1import org.springframework.context.annotation.Profile;
2import org.springframework.stereotype.Service;
3
4@Profile("dev")
5@Service
6public class FakePaymentService implements PaymentService {
7    @Override
8    public String charge(int amount) {
9        return "fake:" + amount;
10    }
11}

This approach is different from @Qualifier. Profiles control bean registration. Qualifiers control which already-registered bean gets injected.

Prefer Constructor Injection for Clarity

Constructor injection makes ambiguity visible immediately and keeps dependencies immutable.

java
1import org.springframework.stereotype.Service;
2
3@Service
4public class BillingFacade {
5    private final PaymentService paymentService;
6
7    public BillingFacade(PaymentService paymentService) {
8        this.paymentService = paymentService;
9    }
10}

If wiring is wrong, the application fails at startup instead of hiding problems in mutable fields. That is a strong default for most Spring Boot services.

Common Pitfalls

  • Expecting Spring to choose between multiple interface implementations without explicit guidance.
  • Using @Primary when the correct implementation actually depends on runtime context.
  • Relying on implicit bean names and breaking qualifiers during refactoring.
  • Switching to field injection instead of fixing the wiring design clearly.
  • Forgetting that profiles and conditions change which beans exist at all.

Summary

  • Multiple implementations make type-based autowiring ambiguous by design.
  • Use @Qualifier when one class needs one specific bean.
  • Use @Primary when one implementation should be the default.
  • Inject a collection when runtime strategy selection is part of the design.
  • Prefer constructor injection so wiring errors fail early and clearly.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Object-Oriented Design practice on Codemia

Turn requirements into classes, and defend the design, on the problems that come up in OOD rounds.

Practice OOD

All Rights Reserved.