Inversion of Control (IoC)
What is control in Java?
Control means who decides what happens in a program:
- which objects get created,
- when they get created,
- and which methods get called.
In a normal Java program, your code controls everything. You create objects with new and call their methods yourself:
public class OrderService {private EmailService emailService = new EmailService(); // OrderService decidespublic void placeOrder(String customer, String product) {emailService.sendEmail(customer, "Order confirmed: " + product);}}
Here, OrderService is in control of its dependency. It decides which class to use (EmailService) and when to create it.
What is Inversion of Control (IoC)?
Inversion of Control (IoC) means handing control to someone else, instead of your code doing everything itself.
"Inversion" just means "flipping". Normally, your code is in charge. With IoC, the control flips: something outside your code (usually a framework) takes charge, and your code just does its part when asked.
1Normal control: Your code → creates objects, calls methods, decides the flow2Inverted: Framework → creates objects, calls your code, decides the flowThink of the difference between driving your own car and taking a taxi:
- Driving yourself: you control everything: the route, the turns, the speed.
- Taking a taxi: you just say where you want to go. The driver handles the rest.
With IoC, your code is the passenger. It says what it needs, and the framework takes care of how and when.
You've probably used IoC already without noticing it. In a JUnit test, you never call your test methods yourself:
class OrderServiceTest {@Testvoid placeOrderSendsConfirmation() {// JUnit calls this method. You never do.}}
You just write the method and mark it with @Test. JUnit decides when to create the test class and when to run each method. That's control handed to someone else.
IoC is often summed up as:
"Don't call us, we'll call you."
In this page, we're interested in one specific kind of control: who creates a class's dependencies. Let's see how that control gets inverted.
How DI changes control flow
With Dependency Injection, OrderService no longer creates its dependency. It only receives one:
public class OrderService {private final NotificationService notificationService;public OrderService(NotificationService notificationService) {this.notificationService = notificationService; // someone else decided}}
// The outside code now decidesOrderService orderService = new OrderService(new SmsNotificationService());
The control has moved:
1Before DI: OrderService decides which notification class to create2After DI: Outside code decides, OrderService just uses what it getsControl used to flow from inside the class. Now it comes from outside. This flip is called Inversion of Control.
IoC vs DI
Inversion of Control (IoC) is the general idea: give control to someone else instead of keeping it in your code.
A common way to describe it:
"Don't call us, we'll call you."
IoC appears in many places, not just with dependencies:
| Example | Who has control |
|---|---|
JUnit runs your @Test methods | JUnit decides when to call them |
| A button click runs your listener | The UI framework decides when to call it |
| Spring creates your objects | Spring decides when to create and connect them |
Dependency Injection is one specific way to do IoC: the control of creating dependencies is moved outside the class.
| IoC | DI | |
|---|---|---|
| What it is | A general idea | A specific technique |
| What is inverted | Any control (calls, flow, creation) | Creating and providing dependencies |
| Relationship | The goal | One way to reach the goal |
In short: IoC is the idea, DI is one way to do it.
Dependency Inversion Principle (DIP) vs Dependency Injection
Both start with "Dependency" and have similar names, so they're often mixed up. They answer different questions.
Dependency Inversion Principle (DIP) is the D in SOLID:
Important classes should depend on interfaces, not on specific classes.
It's about what you depend on.
Dependency Injection is about how you get it: passed in from outside.
You can do one without the other.
DI without DIP: injected, but still depends on a specific class:
public class OrderService {private final EmailService emailService; // specific classpublic OrderService(EmailService emailService) { // but injectedthis.emailService = emailService;}}
You can't switch to SMS, because the type is EmailService.
DIP without DI: depends on an interface, but still creates it:
public class OrderService {private final NotificationService notificationService =new EmailNotificationService(); // interface type, but created inside}
You still can't switch without editing OrderService.
Both together: depends on an interface, and receives it from outside:
public class OrderService {private final NotificationService notificationService; // DIP: interfacepublic OrderService(NotificationService notificationService) { // DI: injectedthis.notificationService = notificationService;}}
| DIP | DI | |
|---|---|---|
| Question it answers | What should I depend on? | How do I get it? |
| Answer | An interface | Passed in from outside |
| Type | Design principle (SOLID) | Technique |
Use them together to get the full benefit.
Why manually managing dependencies becomes difficult
With DI, classes no longer create their dependencies. But someone still has to create every object and connect them:
public class Main {public static void main(String[] args) {MailConfig mailConfig = new MailConfig("smtp.example.com", 587);SmtpClient smtpClient = new SmtpClient(mailConfig);NotificationService notificationService = new EmailNotificationService(smtpClient);DatabaseConfig dbConfig = new DatabaseConfig("jdbc:postgresql://localhost/shop");OrderRepository orderRepository = new OrderRepository(dbConfig);PaymentService paymentService = new PaymentService();OrderService orderService =new OrderService(notificationService, paymentService, orderRepository);}}
That's for one service. As the app grows, this becomes hard:
Order matters. SmtpClient must be created before EmailNotificationService, which must be created before OrderService. Get the order wrong, and it won't compile.
Sharing is manual. If five services need the same OrderRepository, you must create it once and pass the same object to all five yourself.
Changes spread. If EmailNotificationService needs a new dependency, every place that creates it must change.
It grows huge. Real apps have hundreds of classes. This wiring code can grow to hundreds of lines that do nothing but create and connect objects.
Cleanup is manual. Objects like database connections must be closed properly when the app stops.
What we need is something that takes over this job: reads what each class needs, creates objects in the right order, shares them, and cleans them up.
That's an IoC container, and in Spring, it's called the ApplicationContext. That's what we'll learn next.