Dependency injection is a small idea with an outsized effect: a class declares what it needs and something else supplies it. Spring’s contribution is the container that does the supplying.
The application context
At startup Spring builds an ApplicationContext: a registry of bean definitions and
the graph of how they depend on one another. Beans are singletons by default, created
eagerly, and wired before the first request arrives.
Prefer constructor injection
@Service
public class OrderService {
private final OrderRepository orders;
private final PaymentGateway payments;
public OrderService(OrderRepository orders, PaymentGateway payments) {
this.orders = orders;
this.payments = payments;
}
}
Three properties come for free:
- The fields can be
final, so the object is immutable after construction. - The class cannot be built in an invalid state.
- Tests instantiate it directly, with no Spring context at all.
Field injection with @Autowired gives none of those, and it hides the fact that a
class has grown eight collaborators.
Choosing between competing beans
When two beans satisfy the same type, mark the default with @Primary or select
explicitly with @Qualifier.
| Situation | Mechanism |
|---|---|
| One obvious default | @Primary |
| Caller picks per injection point | @Qualifier("name") |
| Depends on environment | @Profile or @ConditionalOnProperty |
The failure mode to watch
Circular dependencies. Spring can sometimes resolve them for you, but a cycle almost
always means the responsibility split is wrong. Extract the shared behaviour into a
third component rather than reaching for @Lazy.