Dependency Injection Vs Dependency Invertion


Dependency Injection (DI) and Dependency Inversion (DIP) are two key concepts in software development that often get confused. While they’re related, they focus on different things. Dependency Injection deals with how objects get their dependencies, making systems easier to test and more modular. Dependency Inversion, part of the SOLID principles, focuses on ensuring high-level code depends on abstractions, not concrete implementations.
In this article, we’ll break down how these ideas work together to boost flexibility, maintainability, and scalability in your code.
Dependency Injection
The hability to inject dependencies trough the controller. According to laravel website,at the service container section we have this:
For example, imagine that you have a UserService
and you want to connect to the database through a Database
class. I’m going to create a class without using DI and I’ll create a new instance of the database class inside the UserService
class:
<?php
class UserService
{
private MySQLDatabase $database;
public function __construct() {
// Warning: Creating a new instance inside the class !!!
$this->database = new MySQLDatabase();
}
public function getUser($id): string {
return $this->database->query("SELECT * FROM users WHERE id = $id");
}
}
class MySQLDatabase
{
public function query($sql): string {
return "User data for ID: $sql";
}
}
$userService = new UserService();
echo $userService->getUser(1);
The result would be:
That defines an Association in Object-Oriented Programming (OOP). Specifically, it is a Composition Association since the UserService
class holds an instance of the MySQLDatabase
class and controls its lifecycle.
Composition is a strong form of association. In it, the "container" (or "whole") class owns the instance of the "part" class. This means that:
If the "container" class is destroyed, the instances of the "part" class are also destroyed.
The "part" class cannot exist independently of the "container" class.
Now let’s rewrite the class like below, now applying DI (Dependency Injection):
<?php
class UserService
{
public function __construct(private MySQLDatabase $database) {}
public function getUser($id): string {
return $this->database->query("SELECT * FROM users WHERE id = $id");
}
}
class MySQLDatabase
{
public function query($sql): string {
return "User data for ID: $sql";
}
}
// depedency from outside
$mysqlDatabase = new MySQLDatabase();
// user service receives depedency from outside
$userService = new UserService($mysqlDatabase);
echo $userService->getUser(1);
Using Dependency Injection (DI) in this example improves flexibility, testability, and maintainability, but it does not fully decouple the system on its own. While DI allows the database dependency to be passed from the outside instead of being instantiated within UserService
, the class is still tied to a specific implementation (MySQLDatabase
). This means DI alone does not eliminate dependency on concrete implementations, it just makes injecting dependencies more manageable.
The Coupling Problem: DIP Can Handle It
Although UserService
now receives the dependency from the outside, it is still tightly coupled to MySQLDatabase
because the dependency type is explicitly defined as MySQLDatabase
. If we needed to switch to PostgreSQLDatabase
or MongoDBDatabase
, we would still have to modify UserService
, which violates the Open/Closed Principle (OCP). This is where the Dependency Inversion Principle (DIP) comes in, promoting the use of interfaces instead of concrete implementations.
According to Robert C. Martin (Uncle Bob) blog, definition of DIP:
In our example, by depending on an interface (DatabaseInterface
), UserService
becomes agnostic to the actual database implementation. This allows us to inject any database class that implements DatabaseInterface
, enabling true flexibility and reducing coupling.
Here's the refactored version following DIP:
<?php
interface DatabaseInterface
{
public function query(string $sql): string;
}
class MySQLDatabase implements DatabaseInterface
{
public function query(string $sql): string
{
return "User data from MySQL for ID: $sql";
}
}
class PostgreSQLDatabase implements DatabaseInterface
{
public function query(string $sql): string
{
return "User data from PostgreSQL for ID: $sql";
}
}
class UserService
{
private DatabaseInterface $database;
public function __construct(DatabaseInterface $database)
{
$this->database = $database;
}
public function getUser(int $id): string
{
return $this->database->query("SELECT * FROM users WHERE id = $id");
}
}
// Injecting MySQLDatabase
$mysqlDatabase = new MySQLDatabase();
$userServiceMySQL = new UserService($mysqlDatabase);
echo $userServiceMySQL->getUser(1);
// Injecting PostgreSQLDatabase
$postgresDatabase = new PostgreSQLDatabase();
$userServicePostgres = new UserService($postgresDatabase);
echo $userServicePostgres->getUser(2);
The result would be:
Do you see? Now UserService
is completely agnostic to the database implementation. It doesn’t need to know any details about it, only how to use it through the interface.
The Dependency Inversion Principle (DIP) makes decoupling ideal because UserService
no longer needs to know about the specific database implementation it interacts with. Instead of depending on a concrete class like MySQLDatabase
, it relies on an abstraction (DatabaseInterface
), allowing any database implementation to be injected without modifying UserService
.
This makes future changes seamless, if a different database like MongoDBDatabase
or PostgreSQLDatabase
is needed, it can be integrated simply by implementing the interface. As a result, the system becomes more flexible, maintainable, and aligned with SOLID principles, particularly Open/Closed (OCP) and Dependency Inversion (DIP).
A Good Use Case: The Service Layer / Repository Pattern
I know the examples are too simple, and one thing I don’t like about them is that the query method is inside the UserService
class. That’s not ideal in real-world applications.
Usually, service layers, if this is about a Layered Architecture, should not access DB implementations through database classes directly. Using a Repository might be better here since it acts as an abstraction over database operations, separating business logic from data access.
This makes it easier to switch database implementations without modifying service logic, improving maintainability, scalability, and testability. Here is an example using all the concepts examplained until now and adding the repository layer.
The structure would be:
/app/Database/DatabaseInterface.php
<?php
declare(strict_types=1);
namespace App\Database;
interface DatabaseInterface
{
public function query(string $sql, array $params = []): array;
}
/app/Database/MySQLDatabase.php
<?php
declare(strict_types=1);
namespace App\Database;
use PDO;
use PDOException;
use RuntimeException;
use App\Database\DatabaseInterface;
class MySQLDatabase implements DatabaseInterface
{
private PDO $connection;
public function __construct()
{
try {
$this->connection = new PDO("mysql:host=localhost;dbname=app", "user", "password");
$this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
throw new RuntimeException(
"Database connection failed: " . $e->getMessage(),
(int) $e->getCode(),
$e
);
}
}
public function query(string $sql, array $params = []): array
{
try {
$stmt = $this->connection->prepare($sql);
$stmt->execute($params);
return $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
} catch (PDOException $e) {
throw new RuntimeException(
"Database query failed: " . $e->getMessage(),
(int) $e->getCode(),
$e
);
}
}
}
/app/Database/PostgreSQLDatabase.php
<?php
declare(strict_types=1);
namespace App\Database;
use PDO;
use PDOException;
use RuntimeException;
use App\Database\DatabaseInterface;
class PostgreSQLDatabase implements DatabaseInterface
{
private PDO $connection;
public function __construct()
{
try {
$this->connection = new PDO("pgsql:host=localhost;dbname=app", "user", "password");
$this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
throw new RuntimeException(
"Database connection failed: " . $e->getMessage(),
(int) $e->getCode(),
$e
);
}
}
public function query(string $sql, array $params = []): array
{
try {
$stmt = $this->connection->prepare($sql);
$stmt->execute($params);
return $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
} catch (PDOException $e) {
throw new RuntimeException(
"Database query failed: " . $e->getMessage(),
(int) $e->getCode(),
$e
);
}
}
}
/app/Repository/UserRepositoryInterface.php
<?php
declare(strict_types=1);
namespace App\Repository;
interface UserRepositoryInterface
{
public function getUserById(int $id): ?array;
}
/app/Repository/DbUserRepository.php
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Database\DatabaseInterface;
use App\Repository\UserRepositoryInterface;
class DbUserRepository implements UserRepositoryInterface
{
public function __construct(private DatabaseInterface $database) {}
public function getUserById(int $id): ?array
{
$sql = "SELECT * FROM users WHERE id = :id";
return $this->database->query($sql, ['id' => $id]);
}
}
/app/Repository/InMemoryUserRepository.php
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Repository\UserRepositoryInterface;
class InMemoryUserRepository implements UserRepositoryInterface
{
private array $users = [
1 => ['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com'],
2 => ['id' => 2, 'name' => 'Bob', 'email' => 'bob@example.com']
];
public function getUserById(int $id): ?array
{
return $this->users[$id] ?? null;
}
}
/app/Service/UserService.php
<?php
declare(strict_types=1);
namespace App\Service;
use App\Repository\UserRepositoryInterface;
class UserService
{
public function __construct(private UserRepositoryInterface $userRepository) {}
public function getUserDetails(int $id): ?array
{
$user = $this->userRepository->getUserById($id);
if (empty($user)) {
return null;
}
$user['email'] = substr($user['email'], 0, 3) . '****@' . explode('@', $user['email'])[1];
return $user;
}
}
This UserService
might look like just a proxy because it simply fetches user details, making it seem redundant. However, in real-world scenarios, service layers handle way more than just CRUD operations.
In complex applications, the service layer is responsible for business rules, validations, aggregating data from multiple sources, handling transactions, caching, and orchestrating different dependencies. The example here is intentionally simple to demonstrate the structure flow, making it easier to understand in the context of an article.
While in this case, it only retrieves a user and masks their email, in production systems, services could be responsible for permission checks, event dispatching, sending notifications, or applying domain logic before returning data. So even though it looks unnecessary now, service layers become crucial as business complexity grows.
/public/index.php
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use App\Database\MySQLDatabase;
use App\Database\PostgreSQLDatabase;
use App\Repository\UserRepository;
use App\Repository\InMemoryUserRepository;
use App\Service\UserService;
$mysqlDatabase = new MySQLDatabase();
$userRepository = new UserRepository($mysqlDatabase);
$userService = new UserService($userRepository);
print_r($userService->getUserDetails(1));
$postgresDatabase = new PostgreSQLDatabase();
$userRepositoryPostgres = new UserRepository($postgresDatabase);
$userServicePostgres = new UserService($userRepositoryPostgres);
print_r($userServicePostgres->getUserDetails(2));
$inMemoryRepo = new InMemoryUserRepository();
$userServiceMemory = new UserService($inMemoryRepo);
print_r($userServiceMemory->getUserDetails(1));
In traditional frameworks like Laravel, Symfony, or Slim, we typically wouldn't manually instantiate dependencies like in this example. Instead, we would leverage the Dependency Injection (DI) container to handle object resolution automatically.
Final Thoughts
The idea is simple: reduce dependency between components. DI handles how dependencies are provided, while DIP ensures code relies on abstractions, not implementations. Together, they keep things modular and adaptable.
They also tie into Agile values, like adaptability and incremental development, helping teams build systems that evolve easily with changing needs.
In short, DI and DIP work hand-in-hand, laying the groundwork for strong, scalable, and modern software designs.
References
SOLID Principles – https://blog.cleancoder.com/uncle-bob/2020/10/18/Solid-Relevance.html
Laravel Dependency Injection Container – https://laravel.com/docs/container
Symfony Dependency Injection Component – https://symfony.com/doc/current/components/dependency_injection.html
Slim Framework Dependency Injection – https://www.slimframework.com/docs/v4/concepts/di.html
Subscribe to my newsletter
Read articles from Lucas Sahdo directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Lucas Sahdo
Lucas Sahdo
🚀 Software Engineer with +3 years working abroad ⏳ +12 years of experience with IT ⚡️ I write code in PHP, JS, TS and GO 👇🏻