Skip to content
C Codeloom
Java

Spring Boot Dependency Injection Explained

How Spring Boot wires your beans: component scanning, constructor injection, qualifiers, profiles, and the testing strategies that follow from each choice.

·4 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • What the Spring ApplicationContext does
  • Component scanning vs explicit @Bean configuration
  • Why constructor injection beats field injection
  • Resolving ambiguity with @Qualifier and @Primary
  • How profiles and conditional beans support testing

Prerequisites

  • Basic familiarity with Java and a build tool like Maven or Gradle

What and Why

Dependency Injection (DI) is a way of structuring code so that objects receive their collaborators from outside instead of constructing them. Spring Boot’s biggest job is to be the container that builds and wires those collaborators for you, so your application code can focus on behavior, not plumbing.

The win is testability: a class that takes its dependencies as constructor parameters can be exercised in isolation with fakes, without any framework running.

Mental Model

Spring maintains an ApplicationContext: a registry of beans keyed by type (and optionally by name). At startup, Spring discovers bean definitions, resolves their dependencies, and instantiates them in dependency order. Your code receives the wired graph and starts handling requests.

@SpringBootApplication
 |
 v
component scan finds @Component / @Service / @Repository / @Controller
 |
 v
@Configuration classes contribute @Bean factory methods
 |
 v
ApplicationContext resolves dependencies (by type, then qualifier)
 |
 v
beans instantiated, lifecycle callbacks invoked, app ready
Spring bean wiring at startup

Hands-on Example

A small service with constructor injection:

import org.springframework.stereotype.*;
import org.springframework.boot.*;
import org.springframework.boot.autoconfigure.*;

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

@Service
class GreetingService {
    private final Clock clock;
    private final GreetingRepository repo;

    public GreetingService(Clock clock, GreetingRepository repo) {
        this.clock = clock;
        this.repo = repo;
    }

    public String greet(String name) {
        repo.recordHit(name, clock.now());
        return "Hello, " + name;
    }
}

@Component
class Clock {
    public long now() { return System.currentTimeMillis(); }
}

@Repository
class GreetingRepository {
    public void recordHit(String name, long ts) { /* ... */ }
}

Spring sees three components, picks the constructor of GreetingService, finds matching beans for Clock and GreetingRepository, and instantiates the graph. No XML, no setters, no static singletons.

When you need to register a bean you do not own, use @Configuration:

@Configuration
class HttpConfig {
    @Bean
    public java.net.http.HttpClient httpClient() {
        return java.net.http.HttpClient.newHttpClient();
    }
}

Resolving ambiguity is straightforward. If two beans match a type, mark one with @Primary or inject by name using @Qualifier("fast").

Common Pitfalls

  • Field injection: @Autowired on a field is convenient but hides dependencies, breaks immutability, and makes unit tests reach for reflection. Prefer constructor injection.
  • Circular dependencies: A needs B, B needs A. Spring will refuse to start (since 2.6 by default). Solutions: redesign, or break the cycle with @Lazy if redesign is impossible.
  • Mutating singleton state: by default beans are singletons. Storing per-request data in a field causes subtle bugs under load. Use method-local state or @Scope("request").
  • Overusing component scanning: scanning unrelated packages slows startup and pulls unintended beans. Keep packages tidy and only scan what you own.
  • Missing @ConfigurationProperties: passing every application.properties value through @Value scatters configuration. Group related properties into a record bound with @ConfigurationProperties.

Practical Tips

Use Lombok’s @RequiredArgsConstructor or Java records for boilerplate-free constructor injection. With Spring 4.3+, a single constructor does not even need @Autowired.

Embrace profiles. Annotate test-only beans with @Profile("test") and production-only beans with @Profile("prod"). Set spring.profiles.active per environment.

@ConditionalOnProperty and @ConditionalOnMissingBean let you write libraries that auto-configure but get out of the user’s way if they override.

For tests, prefer slice annotations (@WebMvcTest, @DataJpaTest) over full @SpringBootTest when possible. They start a smaller context, run faster, and isolate the layer under test.

When debugging a wiring failure, read the message carefully: Spring tells you which dependency was unsatisfied and which candidates were considered. Enabling --debug prints the autoconfiguration report.

Wrap-up

Spring Boot DI is mostly about handing constructors enough information to find their collaborators. Stick to constructor injection, keep your beans stateless, lean on profiles for environment differences, and use slice tests to keep feedback fast. Once those habits are in place, the framework feels less like magic and more like a careful registry that builds your application graph for you.