March 30, 2026
Dependency: Why Inversion, and Not Just Injection
Dependency injection is not dependency inversion. One tells you how to pass dependencies. The other tells you who gets to define their shape. Here's the distinction, built from first principles.
You've probably heard this before: depend on abstractions, not concretions. Maybe you've wired up a DI container, injected a repository into a service, and felt like you had it covered. The D in SOLID? Done.
Not quite. Dependency Injection and the Dependency Inversion Principle are related, but they are not the same thing. One is a technique. The other is a design decision about control. Let me break it down.
The Scenario
Imagine you receive a CSV file that needs processing. Since we respect the Single Responsibility Principle, we create separate classes: one for processing, one for reading. The processor needs to read the file before processing, so it depends on the reader.
The Wrong Way
The thing you must never do: instantiate the file reader inside the file processor.
class FileProcessor
{
private CSVFileReader $reader;
public function __construct()
{
$this->reader = new CSVFileReader();
}
public function processFile(string $path): array
{
return $this->reader->read($path);
}
}
Why is this wrong? FileProcessor creates its own dependency. Testing requires a real CSV file. Swapping formats means editing this class. The two are fused.
The Clean-But-Still-Wrong Version
The natural fix: inject it.
class FileProcessor
{
public function __construct(
private CSVFileReader $reader
) {}
public function processFile(string $path): array
{
return $this->reader->read($path);
}
}
Better. FileProcessor no longer creates the reader. You pass it in. You can swap it at the call site. You can fake it in tests.
That's dependency injection.
Are we done? Did we comply with the D in SOLID?
No. Not yet. So what's the problem?
The Problem
Now suppose you need to handle JSON files, not CSV. You create a JSONFileReader. But to use it, you must edit FileProcessor to accept the new type. Tomorrow XML arrives. You edit it again.
class FileProcessor
{
public function __construct(
private JSONFileReader $reader // changed from CSVFileReader
) {}
// everything else stays the same, but the class signature changed
}
Why is this a problem? It's not scalable. Every new file format forces a change to FileProcessor, a class that should care about processing, not parsing. This violates the Open/Closed Principle: the class is not open for extension without modification.
Injection solved the construction problem. The coupling problem is still there.
The Inversion: Step 1 - Define a Contract
The first step of abstraction: define an interface.
interface FileReader
{
public function read(string $path): array;
}
class FileProcessor
{
public function __construct(
private FileReader $reader
) {}
public function processFile(string $path): array
{
$data = $this->reader->read($path);
return ...;
}
}
Now FileProcessor depends on an abstraction. Any class implementing FileReader can be passed in. Add JSON support? Create JSONFileReader implements FileReader. Add XML? Create XMLFileReader implements FileReader. FileProcessor never changes.
class CSVFileReader implements FileReader
{
public function read(string $path): array
{
// parse CSV and return array
}
}
class JSONFileReader implements FileReader
{
public function read(string $path): array
{
// parse JSON and return array
}
}
This looks like inversion. Are we done? Not yet.
Step 2 - Place the Contract Deliberately
Where does FileReader live in your codebase?
This depends on your codebase structure, but if it lives next to CSVFileReader in the infrastructure layer, you've gained flexibility but not inversion. FileProcessor is still reaching down into infrastructure to borrow an abstraction defined there.
The inversion happens when FileReader lives next to FileProcessor in the application layer. The high-level module defines what it needs. The low-level modules comply with that contract.
This is the core of hexagonal architecture: the domain or application layer defines the ports (interfaces), and the infrastructure layer provides the adapters (implementations). The dependency points inward, not outward.
Application Layer (core logic)
├─ FileProcessor
└─ FileReader (interface)
Infrastructure Layer (implementation details)
├─ CSVFileReader implements FileReader
└─ JSONFileReader implements FileReader
The interface belongs to the consumer. The implementor comes to it. That's the inversion.
Why It Actually Matters
This is not about purity. Here are the concrete consequences.
Testability. Write a FakeFileReader in five lines or Mock it (the dependency). No file system, no complex setup. Test the processing logic in isolation.
Decoupling. Application logic survives infrastructure changes. CSV today, JSON next week, XML next month. FileProcessor never changes. It declared what it needs. Implementations comply. Open/closed principle in action.
Business agility. When product says "we need to support a new file format by Friday," you add one class. The rest is untouched. Two-day task, not a two-week refactor.
Injection gives you flexibility at the call site. Inversion gives you architectural control over which layer leads. That control shapes how fast your system adapts to changing requirements.