Skip to main content

Dependency Injection vs. Dependency Inversion

Hard

Interview Question: "Everyone throws around the terms Dependency Injection, Dependency Inversion, and Inversion of Control. Are they the same thing? What are the three types of DI, and why is field injection considered an anti-pattern?"

Picture an F1 driver who's hardcoded to build their own car: welds the chassis, bolts on the tires, before every race. If it starts raining, they're stuck: they already built dry-weather tires with their own hands, and that's all they know how to make.

Now picture a driver who just says "I need a Vehicle." The pit crew hands them one: dry tires on a sunny day, rain tires if it's wet. The driver never touches tire manufacturing. They just race with whatever Vehicle shows up.

That's the difference dependency injection makes: a class stops building its own dependencies and instead just declares what it needs, letting something outside hand it over.

HARDCODED COUPLING (NO DI)DEPENDENCY INJECTION (DI)new PaymentProcessor()PaymentProcessordb = new MySQLDatabase()createsMySQLDatabaseRigidly locked to MySQLDatabaseImpossible to mock in unit testsnew PaymentProcessor(db)PaymentProcessorconstructor(Database db)<<interface>> DatabaseMySQLDbPostgresDbMockDbSwappable: real DB in prod, mock DB in tests
Without DI, the class builds its own concrete dependency and welds itself to it. With DI, an external caller hands in any implementation that satisfies the interface.

The three ways to inject a dependency​

Injection TypeHow It WorksVerdict
1. Constructor InjectionThe dependency is passed in as a constructor parameter.Best practice. The object can't exist in a half-built state, fields can be final/immutable, and you can unit test with a plain object - no framework needed.
2. Setter InjectionThe dependency is handed over through a public setter method, after construction.Use with caution. Fine for optional dependencies, but if something calls a method before the setter runs, you get a NullPointerException.
3. Field InjectionA private field is annotated (like @Autowired) and the framework fills it in via reflection.Anti-pattern. The dependency is invisible from the constructor, it's hard to unit test without the framework, and circular dependencies don't surface until runtime.

Three terms people mix up​

  • Dependency Inversion (DIP) is the principle: high-level code should depend on interfaces, not concrete classes. This is the goal.
  • Dependency Injection (DI) is the technique: pass dependencies in from outside instead of constructing them internally. This is how you actually achieve DIP.
  • Inversion of Control (IoC) is the framework that does the wiring for you automatically - Spring's ApplicationContext, NestJS's injector - deciding what to construct, in what order, and handing it to whoever asked.

In short: DIP says what the code should look like, DI is how you write it that way, and IoC is the machinery that does it for you at scale.


Clean Code Example​

Here is how constructor dependency injection enables flexible swapping and easy unit testing across languages:

// 1. Abstraction (DIP)
interface Database {
void save(String data);
}

class PostgresDatabase implements Database {
@Override
public void save(String data) {
System.out.println("Saved to Postgres: " + data);
}
}

// 2. Class depends on interface; receives it via Constructor Injection
class PaymentProcessor {
private final Database database; // Immutability guaranteed

public PaymentProcessor(Database database) {
this.database = database; // Injected from outside
}

public void process(String transaction) {
database.save(transaction);
}
}

// Usage in tests: easy to inject a MockDatabase without a framework