Ch. 8 · Spring Boot

Spring Boot: No Qualifying Bean of Type (Could Not Be Found)

Fix 'required a bean of type that could not be found' and 'No qualifying bean' in Spring Boot 3: scanning, conditions, qualifiers, tests.

~7 min readbeginnerupdated Oct 4, 2026

Startup stops and Spring Boot 3.x tells you which constructor could not be satisfied:

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in com.shop.orders.OrderController required a bean of type 'com.shop.orders.OrderService' that could not be found.


Action:

Consider defining a bean of type 'com.shop.orders.OrderService' in your configuration.
Text

Underneath is a NoSuchBeanDefinitionException (“No qualifying bean of type …”). It means Spring tried to build OrderController, looked for any bean assignable to OrderService, and found none. The opposite problem, too many candidates, looks like this:

Description:

Parameter 0 of constructor in com.shop.checkout.CheckoutService required a single bean, but 2 were found:
	- stripeGateway: defined in file [.../StripeGateway.class]
	- paypalGateway: defined in file [.../PaypalGateway.class]
Text

That one is NoUniqueBeanDefinitionException. Both are wiring problems you can diagnose from the message alone.

Quick fix checklist

  • Is the class annotated @Service, @Component, @Repository or produced by a @Bean method? An interface alone is not a bean.
  • Is it in the @SpringBootApplication class’s package or a sub-package?
  • Is it switched off by @Profile, @ConditionalOnProperty or another condition? Run with --debug.
  • For a framework type (JdbcTemplate, RestClient.Builder, KafkaTemplate), is the matching starter on the classpath?
  • For “found 2”, choose with @Primary, @Qualifier, or inject List<T> / Map<String, T>.
  • In a test slice (@WebMvcTest, @DataJpaTest), provide missing collaborators with @MockitoBean.

Before you start

You should know what a Spring bean is and how constructor injection works. This article targets Spring Boot 3.x on Java 17; the testing parts use @MockitoBean, introduced in Spring Framework 6.2 (Boot 3.4), which replaces the now deprecated @MockBean.

Why it happens

Beans enter the context in three ways: component scanning finds classes with a stereotype annotation, @Configuration classes declare @Bean methods, and auto-configuration contributes beans when its conditions match. When Spring resolves a constructor parameter it searches all registered definitions for types assignable to the parameter type.

Zero matches has a small set of causes:

  • Not a component. The class has no stereotype, or the annotation is on the interface instead of the implementation. Scanning also skips abstract classes.
  • Not scanned. @SpringBootApplication scans only its own package and below. A class in com.shop.payments is invisible to com.shop.app.ShopApplication.
  • Excluded by a condition. @Profile("prod") on a bean when prod is not active, or @ConditionalOnProperty when the property is absent, removes the definition entirely.
  • Missing auto-configuration. JdbcTemplate comes from JdbcTemplateAutoConfiguration, which needs spring-jdbc and a DataSource. No starter, no bean.
  • Partial context. Test slices deliberately load a subset of beans.

More than one match happens when several implementations of an interface are beans. Before giving up, Spring narrows the candidates: a @Qualifier on the injection point filters them, a @Primary bean wins, and finally a bean whose name equals the parameter name is chosen. That last fallback needs parameter names in the bytecode; since Spring Framework 6.1 this means compiling with -parameters, which Boot’s Maven parent and Gradle plugin configure for you. Only if all of those fail do you get the “required a single bean” failure.

Step-by-step walkthrough

Step 1: Read which parameter failed

“Parameter 0 of constructor in X” points to the exact injection point; for field injection it says “Field y in X”. The quoted type is what Spring searched for. Check it is the type you meant: injecting the implementation class OrderServiceImpl when only a JDK proxy of the interface exists, or a class with the same simple name from another package, produces the same message.

Step 2: Check whether the bean is defined at all

When the missing bean is an auto-configured one, the failure analysis often adds a section like:

The following candidates were found but could not be injected:
	- Bean method 'restClientBuilder' in 'RestClientAutoConfiguration' not loaded because ...
Text

For your own classes, run with --debug to print the condition evaluation report, and search for the class name. If it is not mentioned anywhere, scanning never saw it. If you see it under “Negative matches”, the listed condition tells you why it was skipped, for example @ConditionalOnProperty (payments.enabled=true) did not find property 'payments.enabled'.

Step 3: Fix scanning and stereotype problems

Put the application class in the root package:

com.shop.ShopApplication
com.shop.orders.OrderController
com.shop.orders.OrderService          // @Service
com.shop.payments.PaymentGateway
Text

Annotate the implementation, not the interface:

public interface OrderService {
    List<Order> openOrders();
}

@Service
class DefaultOrderService implements OrderService {
    @Override
    public List<Order> openOrders() {
        return List.of();
    }
}
java

For beans in a library jar with a different root package, prefer an explicit @Import(PaymentsConfiguration.class) or the library’s own auto-configuration over widening scanBasePackages.

Step 4: Resolve multiple candidates on purpose

Pick the strategy that matches the domain:

public interface PaymentGateway {
    Receipt charge(Money amount);
}

@Component
@Primary
class StripeGateway implements PaymentGateway { /* ... */ }

@Component
class PaypalGateway implements PaymentGateway { /* ... */ }

@Service
class CheckoutService {
    private final PaymentGateway defaultGateway;
    private final Map<String, PaymentGateway> gatewaysByBeanName;

    CheckoutService(PaymentGateway defaultGateway,
                    Map<String, PaymentGateway> gatewaysByBeanName) {
        this.defaultGateway = defaultGateway;
        this.gatewaysByBeanName = gatewaysByBeanName;
    }
}
java

@Primary sets a default for the whole application. @Qualifier("paypalGateway") on a parameter picks one explicitly at that injection point. Injecting List<PaymentGateway> or Map<String, PaymentGateway> (keyed by bean name) is right when the code genuinely chooses at runtime, such as a strategy per customer country.

Step 5: Fix test slices

@WebMvcTest registers controllers, @ControllerAdvice, filters, converters and MVC configuration, but not @Service or @Repository beans. That is the point of a slice, so supply collaborators as mocks rather than widening the test.

Worked scenario

A controller test that passed for months suddenly fails after OrderController gains a dependency:

@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired
    MockMvc mvc;

    @Test
    void listsOpenOrders() throws Exception {
        mvc.perform(get("/api/orders")).andExpect(status().isOk());
    }
}
java

The failure: “Parameter 0 of constructor in com.shop.orders.OrderController required a bean of type ‘com.shop.orders.OrderService’ that could not be found.” The application itself starts fine, which is the key clue: the bean exists in the full context but not in the slice.

The fix is to declare the collaborator as a mock and give it behaviour:

import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired
    MockMvc mvc;

    @MockitoBean
    OrderService orderService;

    @Test
    void listsOpenOrders() throws Exception {
        given(orderService.openOrders()).willReturn(List.of(new Order(7L, "OPEN")));

        mvc.perform(get("/api/orders"))
           .andExpect(status().isOk())
           .andExpect(jsonPath("$[0].id").value(7));
    }
}
java

On Spring Boot versions before 3.4, use @MockBean from org.springframework.boot.test.mock.mockito; it still works on 3.4 and later but is deprecated for removal.

Common mistake

The most tempting wrong fix is adding @ComponentScan("com.shop") next to @SpringBootApplication. A directly declared @ComponentScan does not inherit the exclude filters that @SpringBootApplication’s own scan registers (TypeExcludeFilter and AutoConfigurationExcludeFilter), so you can end up scanning auto-configuration classes or test-only configurations unexpectedly. If you must widen scanning, use @SpringBootApplication(scanBasePackages = "com.shop"), and better still, move the main class to the root package.

Another: marking the dependency optional with @Autowired(required = false) or Optional<OrderService> to make startup pass. The controller then fails at runtime with a NullPointerException or an empty optional. Optional injection is for genuinely optional features, not for missing beans.

For multiple candidates, putting @Primary on whichever bean happens to fix the error is risky: it silently changes the choice for every other injection point in the application, including ones that previously used parameter-name matching.

Verify the behavior

For conditional beans, ApplicationContextRunner proves both states quickly:

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

class PaymentsConfigurationTest {

    private final ApplicationContextRunner runner = new ApplicationContextRunner()
            .withUserConfiguration(PaymentsConfiguration.class);

    @Test
    void registersGatewayWhenEnabled() {
        runner.withPropertyValues("payments.enabled=true")
              .run(context -> assertThat(context).hasSingleBean(PaymentGateway.class));
    }

    @Test
    void skipsGatewayWhenDisabled() {
        runner.run(context -> assertThat(context).doesNotHaveBean(PaymentGateway.class));
    }
}
java

Here PaymentsConfiguration is a @Configuration class whose @Bean method is annotated @ConditionalOnProperty(name = "payments.enabled", havingValue = "true"). Add one plain @SpringBootTest context-load test so a missing bean in the full application fails in CI rather than on deploy.

Interview exercise

“An interface NotificationSender has one implementation, EmailSender, injected into several services as NotificationSender sender. A developer adds SmsSender and the application stops starting. Explain the failure and give two fixes with their trade-offs.”

Answer and reasoning

With one implementation, resolution by type was unambiguous. Adding a second @Component gives every NotificationSender injection point two candidates. Spring tries its tie-breakers (a qualifier, a @Primary bean, a bean named like the parameter sender), none apply, and startup fails with “required a single bean, but 2 were found”. One fix is @Qualifier at that injection point, which keeps the choice local and explicit but couples the consumer to a bean name. Another is to inject List<NotificationSender> and route by a method such as supports(channel), which suits code that should send through every or a chosen channel and makes adding a third sender a non-event. @Primary would also work, but it is a global decision, so I would use it only when one implementation really is the default everywhere.

Continue learning

More in Spring Boot

read ✓Spring Boot · hard

Spring Boot Testcontainers

Test against real databases and services in containers, manage their lifecycle, and isolate state between tests.

~2 min readread →
esc