Spring DI and the IoC Container
Inversion of control and dependency injection, how the Spring container registers and looks up beans, and the four injection styles
Contents
Moving control over object creation and wiring outside your code is inversion of control (IoC), and implementing that wiring through external injection is dependency injection (DI).
Who creates and wires the objects
Before Spring, an implementation object created and selected its own dependencies. An order service would call new on a discount policy implementation directly. That puts the implementation object in charge of the program's control flow.
The problem is that client code becomes bound to an implementation rather than an abstraction. Switching the discount policy from a fixed amount to a percentage forces a change in the code that constructed the object. Using an object and creating it are two responsibilities tangled in one place.
The fix is to extract a configuration object responsible only for creation and wiring. It is conventionally called AppConfig. Once AppConfig exists, implementation objects run only their own logic and never learn which implementation was injected.
Handing control of the program flow to something outside your code is inversion of control (IoC). The same distinction separates frameworks from libraries. If the framework calls the code you wrote, it is a framework, with JUnit as the canonical example. If your code drives the flow itself, it is a library.
What dependency injection means
Dependency injection (DI) is the primary way IoC gets implemented. Instead of constructing dependencies internally, an object receives references to them from outside. The mechanics are simple: create the object instances, then pass the references in to connect them.
Separating static from dynamic dependencies makes the payoff clear. At the class level, the fact that OrderServiceImpl depends on the DiscountPolicy interface is fixed at compile time. Which implementation gets connected at runtime, however, can be changed through injection.
That is what makes it possible to swap implementations without touching client code. Because the code depends only on abstractions, it satisfies the dependency inversion principle (DIP). It also follows the open-closed principle (OCP), open to extension and closed to modification, because the wiring area and the usage area are separate.
The Spring container and beans
Something that creates objects and wires their dependencies, as AppConfig does, is called an IoC container or DI container. In Spring this container is exposed through the ApplicationContext interface. When building a container from a Java configuration class, you use the implementation AnnotationConfigApplicationContext.
Annotate the configuration class with @Configuration and return the objects to register from @Bean methods. The container calls every @Bean method, registers the returned objects, and uses the method names as bean names.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}Registered beans are retrieved from the container by name and type. Code that used to look up objects itself now looks them up in the container.
ApplicationContext ac =
new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService =
ac.getBean("memberService", MemberService.class);ApplicationContext extends BeanFactory, which provides bean management, and adds features such as message handling and resource lookup. Since there is rarely a reason to use BeanFactory directly, ApplicationContext is what people mean by the Spring container.
Manual registration versus component scanning
Listing every bean with @Bean makes configuration grow and invites omissions as the count rises. Spring offers component scanning, which registers beans automatically without listing them in configuration. Adding @ComponentScan to a configuration class finds classes annotated with @Component and registers them as beans.
@Configuration
@ComponentScan(basePackages = "hello.core")
public class AutoAppConfig {
}The bean name here is the class name with the first letter lowercased. MemberServiceImpl becomes memberServiceImpl. Setting basePackages narrows the starting point of the scan so unrelated packages are not walked.
Alongside automatic registration, dependencies are wired automatically with @Autowired. Annotating a constructor with @Autowired makes the container find a bean of the matching type and inject it, which behaves exactly like calling getBean by type. In practice the layer-specific annotations @Controller, @Service, and @Repository are used more often, and each contains @Component, so they are picked up by the scan.
| Aspect | Manual registration (@Bean) | Component scan (@Component) |
|---|---|---|
| Registration | Listed as methods in configuration | Registered automatically from annotations |
| Bean name | Method name | Class name with lowercase first letter |
| Fits | Technical infrastructure, a few beans whose intent should be explicit | The bulk of ordinary application logic |
The usual split is to register ordinary application logic through scanning and register manually only the handful of beans where the boundary matters.
Four injection styles
Dependency injection comes in four forms: constructor injection, setter injection, field injection, and general method injection. They differ in injection timing, mutability, and testability.
| Method | Injected when | final fields | Recommendation |
|---|---|---|---|
| Constructor injection | Once, at object creation | Allowed | Default choice |
| Setter injection | After creation, via setter call | Not allowed | Only for optional dependencies |
| Field injection | After creation, via reflection | Not allowed | Not recommended |
| Method injection | After creation, via method call | Not allowed | Rarely used |
Constructor injection is the default. A constructor runs exactly once when the object is created, which matches immutable, required dependencies. If a class has only one constructor, @Autowired can be omitted and injection still happens.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired // optional when there is a single constructor
public OrderServiceImpl(MemberRepository memberRepository,
DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}Three reasons make constructor injection the default. Fields can be final, which catches a missing injection at compile time. Testing with plain Java, free of the framework, needs nothing more than passing values to the constructor. And required dependencies are visible in the constructor signature, which makes the intent explicit.
Setter injection is reserved for dependencies that are optional or may change later. @Autowired fails when no matching bean exists, so use @Autowired(required = false) when the code should work without one. Field injection is short to write but offers no way to set values from outside, which makes it weak for testing and change, so it is better avoided.
When two beans share a type
@Autowired resolves by type. If two or more beans share a type, there is no way to decide which one to inject and the wiring fails. The intent has to be stated explicitly.
@Qualifier puts a matching label on both the injection point and the bean. @Primary gives one bean priority among those of the same type. If you need every bean of that type, receive them as a List or Map and get them all at once.
@Component
public class OrderServiceImpl implements OrderService {
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(@Qualifier("rateDiscountPolicy")
DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}The default bean scope also shapes injection design. Per the official Spring documentation the default scope is singleton, and the container shares and reuses one instance. Putting per-request state in a bean field therefore creates concurrency problems, so service beans are designed to be stateless.
Summary
IoC is the idea of moving control over object creation and wiring outside your code, and DI is the technique that implements that wiring through external injection. The Spring container, ApplicationContext, gathers beans through manual @Bean registration and automatic @ComponentScan registration. Those beans are then injected automatically by type. Among the injection styles, constructor injection is the default because it exposes immutable required dependencies and catches omissions at compile time. When beans of the same type collide, state the intent with @Qualifier or @Primary, and since the default scope is singleton, design beans to be stateless.