Two terms appear constantly in Android architecture discussions — Dependency Injection (DI) and Inversion of Control (IoC). They sound similar, they are related, but they solve slightly different problems. The best way to understand both is to watch a single example evolve through three stages.
The Setup: Car, Engine, Motor
Imagine three classes:
Motor— the lowest-level componentEngine— depends on aMotorCar— depends on anEngine
Car cannot function without Engine, and Engine cannot function without Motor. That relationship — where one class cannot do its job without another — is called a dependency. Engine is a dependency of Car.
V0 — Hardcoded Dependencies (The Wrong Way)
public class Motor {
public Motor() {}
}
public class Engine {
private final Motor motor;
public Engine() {
this.motor = new Motor(); // creates dependency internally
}
}
public class Car {
private final Engine engine;
public Car() {
this.engine = new Engine(); // creates dependency internally
}
}
To use a Car:
Car car = new Car(); // one line — looks clean
Seems convenient. But there are real problems hiding here:
Caris tightly coupled toEngine, andEngineis tightly coupled toMotor.- If you ever need a different kind of
Engine, you have to modifyCaritself. - Unit testing
Carin isolation is impossible — you cannot swap the realEnginefor a fake one. - Every
new Car()silently creates a newEngineand a newMotor— no sharing, no control.
V1 — Dependency Injection
The fix is straightforward: instead of creating dependencies inside a class, pass them in from outside.
public class Motor {
public Motor() {}
}
public class Engine {
private final Motor motor;
public Engine(Motor motor) { // Motor is injected
this.motor = motor;
}
}
public class Car {
private final Engine engine;
public Car(Engine engine) { // Engine is injected
this.engine = engine;
}
}
The caller is now responsible for assembling the graph:
public void createCar() {
Motor motor = new Motor();
Engine engine = new Engine(motor);
Car car = new Car(engine);
}
This is Dependency Injection — the dependencies are provided (injected) from outside the class rather than created inside it.
What improved:
Carno longer decides whichEngineit uses — the caller does.- You can hand
Cara mockEnginein a unit test. - Each class has one responsibility: using its dependencies, not constructing them.
The cost: The caller has to build the whole chain manually. For 3 classes that is fine. For a real project with 20 chained dependencies, it becomes a lot of boilerplate — which is exactly why frameworks like Dagger exist.
V2 — Inversion of Control
V1 still passes concrete classes. If tomorrow you need a TurboEngine instead of Engine, every call site that builds a Car needs to change.
Inversion of Control takes DI one step further: pass an interface, not a class. The class depends on a contract, not an implementation.
public interface Motor {
void run();
}
public class FastMotor implements Motor {
public void run() { /* high RPM logic */ }
}
public class SlowMotor implements Motor {
public void run() { /* economy logic */ }
}
public class Engine {
private final Motor motor;
public Engine(Motor motor) { // accepts the interface
this.motor = motor;
}
}
public class Car {
private final Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
}
Now the caller chooses the behaviour:
// High-performance car
Motor motor = new FastMotor();
Engine engine = new Engine(motor);
Car fastCar = new Car(engine);
// Economy car — same Car class, different behaviour
Motor motor = new SlowMotor();
Engine engine = new Engine(motor);
Car slowCar = new Car(engine);
Car and Engine are completely unchanged. You swap behaviour purely by choosing which implementation to inject. This is polymorphism through interfaces, enabled by Inversion of Control.
DI vs IoC — The Exact Difference
| Dependency Injection | Inversion of Control | |
|---|---|---|
| What you pass | A concrete class | An interface |
| Flexibility | Fixed implementation | Swap implementations freely |
| Testing | Can inject a subclass | Can inject any conforming mock |
| Coupling | Low | Lowest |
Rule of thumb: If you inject a class, it’s DI. If you inject an interface, it’s IoC. IoC is a superset — you cannot have IoC without DI, but you can have DI without IoC.
Why Good Code Means More Lines
One observation worth sitting with: following these principles forces you to write more code. Comparing V0 and V2, the call site grew from one line to four. That trade-off is intentional.
The extra lines buy you:
- Testability — swap any dependency with a test double.
- Flexibility — change behaviour without touching the classes that use it.
- Maintainability — each class has one job; changes stay local.
A DI framework like Dagger automates the boilerplate. It reads annotations on your classes and generates the assembly code — the new Motor(), new Engine(motor), new Car(engine) chain — so you never have to write it by hand. But understanding why that chain needs to exist is essential before letting a framework hide it.