Spring Dependency Injection
At the heart of Spring Boot is the Inversion of Control (IoC) container. Instead of classes creating their own dependencies (new OrderRepository()), they declare what they need, and Spring constructs objects (beans), wires them together, and manages their lifecycle. This dependency injection (DI) makes code modular, testable (swap real dependencies for fakes), and configurable per environment without changing business logic.
Spring Boot adds auto-configuration on top: based on the libraries on your classpath and your properties, it defines sensible beans for you: a DataSource, a Jackson ObjectMapper, web server infrastructure. Understanding how beans are discovered, created, and proxied explains most Spring "magic", and most Spring startup errors.
TL;DR
- The ApplicationContext is the IoC container: it creates, wires, and manages beans.
- Component scanning registers classes annotated with
@Component,@Service,@Repository,@Controller, and@Configuration. - Prefer constructor injection with
finalfields. It's explicit, immutable, and easy to test. - Define third-party or custom-built objects with
@Beanmethods in@Configurationclasses. - Resolve ambiguity with
@Qualifieror@Primary; vary beans by environment with@Profileand conditional annotations. - Features like
@Transactional,@Async, and@Cacheablework through proxies, so self-invocation bypasses them.
Quick Example
Core Concepts
The IoC Container
The ApplicationContext reads bean definitions (from scanning, @Bean methods, and auto-configuration), resolves dependencies, instantiates beans in the right order, injects them, runs lifecycle callbacks, and shuts them down cleanly. SpringApplication.run(App.class) creates the context for a Boot app, and @SpringBootApplication combines @Configuration, @EnableAutoConfiguration, and @ComponentScan from the main class's package downward.
Defining Beans
@Repository also enables exception translation for persistence exceptions; the stereotypes otherwise mainly communicate intent.
Injection Styles
- Constructor injection (recommended): dependencies are required, fields can be
final, and the class is usable without Spring in tests. With a single constructor,@Autowiredis optional. - Setter injection: for optional dependencies that can change.
- Field injection (
@Autowiredon fields): concise, but hides dependencies, prevents immutability, and makes tests require reflection or a Spring context. Avoid it in production code.
Resolving Multiple Candidates
When several beans match a type, Spring needs a hint:
@Primarymarks the default choice.@Qualifier("name")on the injection point selects a specific bean.- Inject
List<PaymentGateway>orMap<String, PaymentGateway>to get all implementations (strategy pattern). ObjectProvider<T>for optional or lazy dependencies.
Scopes and Lifecycle
Singletons are shared across threads, so keep them stateless or thread-safe. Lifecycle hooks: constructor → dependency injection → @PostConstruct → in use → @PreDestroy on shutdown.
Profiles and Conditions
@Profile("prod")activates beans for specific profiles (spring.profiles.active=prod).- Conditional annotations power auto-configuration and your own modular config:
@ConditionalOnProperty,@ConditionalOnClass,@ConditionalOnMissingBean(define a default only if the user hasn't). @ConfigurationPropertiesbinds typed configuration (for examplepayments.*) to records or classes, which is cleaner than scattered@Valueannotations.
Auto-Configuration
Spring Boot ships hundreds of auto-configuration classes. Add spring-boot-starter-data-jpa plus a JDBC driver and Boot configures a DataSource, EntityManagerFactory, and transaction manager. Most are guarded by @ConditionalOnMissingBean, so defining your own bean replaces the default. Start with --debug (or /actuator/conditions) to see which auto-configurations applied and why. See Spring Boot Actuator.
Proxies and AOP
Spring implements cross-cutting features (@Transactional, @Cacheable, @Async, @PreAuthorize, @Retryable) by wrapping beans in proxies. Callers get the proxy, which runs the extra behavior around the real method. Consequences:
- Self-invocation (
this.otherMethod()) skips the proxy, so annotations on the called method don't apply. - Proxied methods must be public and non-final (for class-based proxies).
Best Practices
Use Constructor Injection Everywhere
It makes dependencies explicit, supports immutability, and makes circular dependencies fail fast at startup instead of hiding. Lombok's @RequiredArgsConstructor removes the boilerplate.
Depend on Interfaces at Boundaries
Inject interfaces for external systems (payment gateways, email senders, clocks), so tests and environments can swap implementations. There's no need for interfaces on every internal service. See SOLID principles.
Keep Beans Stateless
Singleton beans serve concurrent requests. Mutable fields (per-request data, counters without synchronization) cause race conditions. Keep state in method variables, databases, or dedicated thread-safe components.
Group Configuration by Feature
Organize @Configuration classes per feature or module, use @ConfigurationProperties for typed settings, and avoid one giant config class.
Common Mistakes
Self-Invocation Bypassing @Transactional
Move generate into another bean, or restructure so the transactional boundary is the entry point.
Circular Dependencies
A needs B and B needs A. Spring Boot 2.6+ rejects circular references by default. That's usually a design smell: extract the shared logic into a third bean, or use events.
NoUniqueBeanDefinitionException
Two beans of the same type without @Primary or @Qualifier make injection ambiguous. Mark a default, or qualify the injection point.
FAQ
What is a Spring bean?
An object whose creation, wiring, and lifecycle are managed by the Spring IoC container. Beans are usually singletons defined via component scanning, @Bean methods, or auto-configuration, and they're injected wherever they're needed.
Why is constructor injection preferred over @Autowired fields?
Constructor injection makes dependencies explicit and required, allows final fields, makes classes easy to instantiate in plain unit tests, and exposes design problems like too many dependencies or circular references. Field injection hides all of that.
How do I replace an auto-configured bean?
Define your own bean of the same type. Most auto-configurations use @ConditionalOnMissingBean and back off. Alternatively, customize via properties or dedicated customizer beans (for example Jackson2ObjectMapperBuilderCustomizer), or exclude an auto-configuration class.
What's the difference between @Component and @Bean?
@Component (and its stereotypes) marks your class for automatic discovery. @Bean is a method in a @Configuration class that returns an instance, which is ideal for third-party classes you can't annotate, or when construction needs logic and parameters.
Related Topics
- Spring Boot — The framework overview
- Spring Data JPA — Repositories as injected beans
- Spring Security — Security configuration via beans
- Spring Boot Testing — Replacing beans in tests
- Java — The language underneath
- SOLID Principles — Dependency inversion