IoC Containers
Inversion of Control (IoC) is the principle behind almost everything Spring does: instead of your code creating and wiring its own dependencies, you hand that responsibility to a container. The container reads your configuration, creates the objects your application needs (called beans), and injects them wherever they’re required.
What the container actually does
A Spring IoC container is responsible for three things:
- Instantiating beans — calling constructors so you don’t have to.
- Configuring beans — setting properties, calling setters.
- Wiring beans together — passing one bean into another’s constructor or setter.
A minimal example
@Servicepublic class UserService {
private final UserRepository repository;
public UserService(UserRepository repository) { this.repository = repository; }
public User findById(Long id) { return repository.findById(id) .orElseThrow(() -> new NotFoundException(id)); }}Notice that UserService never calls new UserRepository(). It simply declares what it needs in its
constructor (line 5, highlighted above), and the container supplies it. This is dependency
injection — the mechanism the IoC container uses to satisfy inversion of control.
Two containers, one idea
The same principle — “don’t construct your own dependencies, ask for them” — shows up outside Java
too. Here’s the same idea in Java (Spring) and Python (a lightweight punq-style container):
public interface Notifier { void send(String message);}
@Componentpublic class EmailNotifier implements Notifier { public void send(String message) { System.out.println("Email: " + message); }}
@Servicepublic class OrderService { private final Notifier notifier;
public OrderService(Notifier notifier) { this.notifier = notifier; // injected by the IoC container }}import punq
class Notifier: def send(self, message: str) -> None: ...
class EmailNotifier(Notifier): def send(self, message: str) -> None: print(f"Email: {message}")
class OrderService: def __init__(self, notifier: Notifier): self.notifier = notifier # injected by the container
container = punq.Container()container.register(Notifier, EmailNotifier)container.register(OrderService)
service = container.resolve(OrderService)Bean scopes
| Scope | Lifetime |
|---|---|
singleton (default) | One instance per container |
prototype | A new instance every time it’s requested |
request | One instance per HTTP request (web apps only) |
session | One instance per HTTP session (web apps only) |
Why this matters
Without an IoC container, every class would need to know how to construct every dependency it uses — including their dependencies. That coupling makes code hard to test (you can’t easily substitute a mock) and hard to change (a constructor signature change ripples through the whole call graph). The container removes that coupling: classes declare what they need, not how to build it.
Once you’re comfortable with the container itself, the natural next step is looking at dependency injection in more depth — specifically the different ways Spring lets you inject dependencies.