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

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

Resolving Multiple Candidates

When several beans match a type, Spring needs a hint:

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

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:

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

References