IoC Containers

2 min readUpdated January 10, 2026springiocdependency-injection

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:

  1. Instantiating beans — calling constructors so you don’t have to.
  2. Configuring beans — setting properties, calling setters.
  3. Wiring beans together — passing one bean into another’s constructor or setter.
Diagram of the Spring IoC container wiring UserRepository into UserService
The container creates both beans and wires UserRepository into UserService automatically.

A minimal example

UserService.java
@Service
public 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);
}
@Component
public class EmailNotifier implements Notifier {
public void send(String message) {
System.out.println("Email: " + message);
}
}
@Service
public 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

ScopeLifetime
singleton (default)One instance per container
prototypeA new instance every time it’s requested
requestOne instance per HTTP request (web apps only)
sessionOne 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.