If you study software development, you have probably been confused by DI, IoC, and DIP at least once.
When courses and articles say things like “an IoC container injects dependencies through DI to follow DIP,” it is easy to lose track of what each term means.
Here is the conclusion first: the three terms operate at different levels. DIP is the principle—why things should be done that way. IoC is the broad direction that realizes the principle—who has control. DI is the concrete technique—how dependencies are supplied.
After reading this article, you should understand how the three concepts connect and be able to answer interview questions about them without hesitation. I have kept the code examples as short as possible.
A one-line summary of the three terms
It is much easier if you memorize one line for each term first.
- DIP (Dependency Inversion Principle): a design principle that says to depend on abstractions, such as protocols, rather than concrete implementations.
- IoC (Inversion of Control): a structure in which the framework or container, rather than your code, controls the program’s flow.
- DI (Dependency Injection): a technique that supplies required objects from outside instead of creating them internally.
The relationship among the three looks like this.
DIP is the goal and principle, IoC is the broader concept used to achieve it, and DI is one concrete way to implement IoC.
In short, the level of abstraction decreases in this order: DIP > IoC > DI.
DIP is about what to depend on
DIP is the D in SOLID, the Dependency Inversion Principle among the five core object-oriented design principles.
The key is twofold: high-level modules should not depend on low-level modules, and both should depend on abstractions.
That sounds abstract, but it becomes much easier with code.
Consider a bad example in which an order service is directly tied to a specific payment provider, Kakao Pay.
// Bad example: directly depending on a concrete class
class OrderService {
private let pay = KakaoPay() // Changing the payment provider requires tearing this out
}
To switch to Naver Pay later, you would have to modify the OrderService code directly. Every new payment method would shake up the high-level module.
DIP reverses this. You define an abstraction—a payment protocol—and depend only on it.
protocol PayGateway { func pay(amount: Int) }
class OrderService {
private let pay: PayGateway // Depend on an abstraction, not a concrete class
init(pay: PayGateway) { self.pay = pay }
}
Whether it is Kakao Pay or Naver Pay, you only need to implement PayGateway; OrderService does not need to change. That is what it means for the dependency direction to be inverted.
IoC is about who has control
IoC, or Inversion of Control, is a somewhat broader concept.
Normally, the code we write controls the entire flow: we create objects, call methods, and determine the order of execution.
IoC reverses that control. Instead of us calling the framework, the framework calls us.
It is often called the Hollywood Principle: “Don’t call us, we’ll call you.”
With a DI container such as Swinject, the container creates, manages, and wires objects instead of you creating them directly. Control moves from you to the container.
Here is one important point: IoC is not limited to DI.
- A framework invoking a callback
- The Template Method pattern
- UIKit invoking lifecycle methods such as viewDidLoad on your behalf
These are all examples of IoC. DI is only a sub-concept specialized in supplying dependencies.
DI is about how dependencies are supplied
DI, or Dependency Injection, is the most representative technique for implementing IoC.
In the earlier DIP example, OrderService received PayGateway through its initializer. That is DI: supplying a required object from outside instead of creating it internally.
There are three main injection methods.
| Injection method | Description | Recommendation |
|---|---|---|
| Constructor injection | Receive the dependency through init | Most recommended (immutable, required dependencies are explicit) |
| Method injection | Receive it as a method parameter | For optional dependencies |
| Property injection | Assign it to a property after initialization | Not recommended because it becomes optional and var |
Swift recommends initializer injection. The dependency can be fixed with let, making it safer, and fake objects (Mocks) are easy to provide in tests.
The relationship can be summarized like this.
To follow the DIP principle, transfer control in the direction of IoC and use DI as the concrete means.
This sentence is the most concise description of the relationship among the three terms.
Frequently asked questions
Q. Aren’t DI and IoC the same thing?
No. IoC is the broader concept, while DI is one of several ways to implement IoC. All DI is IoC, but not all IoC is DI.
Q. Does following DIP automatically mean using DI?
Not necessarily. DIP is the principle “depend on abstractions.” DI begins when an implementation of that abstraction is supplied from outside. The principle and the technique are separate.
Q. Can I use DI without a library such as Swinject?
Yes. Passing a dependency directly through an initializer, as in the Swift example above, is also DI. Swinject and Factory only automate the process; DI itself does not require a library.
To summarize: DIP is why—the principle; IoC is who—the control; and DI is how—the technique. Remembering the three questions this way will keep them clear.
Once you understand that they operate at different levels, DI-related code looks completely different. I hope this article helped you organize the concepts clearly. You’ve got this!

