Dependency Injection
Dependency Injection is a software design technique used to make object-oriented code more flexible, testable, and maintainable. It allows a class to receive the objects it needs from the outside instead of creating them internally.
In Object-Oriented Programming, a dependency is any object, service, or component that another class needs to perform its work. Dependency Injection helps manage these dependencies in a clean way and reduces tight coupling between classes.
Introduction
In previous articles of this OOP and Design Patterns series, we discussed important concepts such as classes, objects, encapsulation, inheritance, polymorphism, abstraction, interfaces, design patterns, Repository Pattern, Strategy Pattern, Observer Pattern, and Command Pattern.
Dependency Injection is strongly connected to many of these concepts. It works especially well with interfaces, abstraction, polymorphism, repositories, services, and clean architecture.
Many modern frameworks such as Laravel, Symfony, Spring, NestJS, and ASP.NET use Dependency Injection heavily. Understanding it helps developers write cleaner code and understand how professional backend frameworks manage services.
What Is Dependency Injection?
Dependency Injection is a technique where a class receives the objects it depends on from an external source instead of creating them itself.
In simple terms, instead of this class saying “I will create what I need,” the class says “give me what I need.”
For example, an OrderService may need a PaymentService and an EmailService. Without Dependency Injection, the OrderService creates these services inside itself. With Dependency Injection, these services are passed into the OrderService from the outside.
What Is a Dependency?
A dependency is any object or service that another class needs to use. If a class cannot complete its job without another class, that other class is a dependency.
For example:
A UserService may depend on a UserRepository.
An OrderService may depend on a PaymentGateway.
A ReportService may depend on a PdfExporter.
A NotificationService may depend on an EmailSender.
A CheckoutService may depend on a DiscountStrategy.
Dependency Injection gives these dependencies to the class instead of forcing the class to create them directly.
Problem Without Dependency Injection
Without Dependency Injection, a class may create its dependencies internally using the new keyword.
class OrderService
{
private PaymentService $paymentService;
private EmailService $emailService;
public function __construct()
{
$this->paymentService = new PaymentService();
$this->emailService = new EmailService();
}
public function placeOrder(Order $order): void
{
$this->paymentService->charge($order);
$this->emailService->sendConfirmation($order);
}
}This code works, but it has a design problem. OrderService is tightly coupled to PaymentService and EmailService. It decides which concrete classes to use and how to create them.
If the application later needs to use a different payment provider, a fake payment service for testing, or a different email sender, the OrderService class must be modified.
Solution with Dependency Injection
With Dependency Injection, the dependencies are passed into the class from outside.
class OrderService
{
public function __construct(
private PaymentService $paymentService,
private EmailService $emailService
) {
}
public function placeOrder(Order $order): void
{
$this->paymentService->charge($order);
$this->emailService->sendConfirmation($order);
}
}Now OrderService does not create the dependencies itself. It receives them through the constructor. This makes the class easier to configure, easier to test, and easier to change.
Why Dependency Injection Is Important
Dependency Injection is important because it reduces tight coupling between classes. A class should focus on its main responsibility, not on creating and configuring all the services it needs.
When dependencies are injected, the class becomes more flexible. Different implementations can be passed depending on the environment, configuration, or test case.
This is especially important in large applications where services may have many dependencies and where code must be tested reliably.
Dependency Injection and Interfaces
Dependency Injection becomes more powerful when combined with interfaces. Instead of depending on a concrete class, a service can depend on an interface.
For example, instead of depending directly on StripePaymentGateway, the CheckoutService can depend on PaymentGatewayInterface.
interface PaymentGatewayInterface
{
public function charge(Order $order): bool;
}
class StripePaymentGateway implements PaymentGatewayInterface
{
public function charge(Order $order): bool
{
// Charge using Stripe
return true;
}
}
class PayPalPaymentGateway implements PaymentGatewayInterface
{
public function charge(Order $order): bool
{
// Charge using PayPal
return true;
}
}Now the application can inject any payment gateway that implements the same interface.
Interface-Based Dependency Injection Example
class CheckoutService
{
public function __construct(
private PaymentGatewayInterface $paymentGateway
) {
}
public function checkout(Order $order): bool
{
return $this->paymentGateway->charge($order);
}
}The CheckoutService does not know whether the payment is processed by Stripe, PayPal, bank transfer, or another provider. It only depends on the PaymentGatewayInterface.
This makes the system easier to extend. A new payment provider can be added without changing the CheckoutService.
Types of Dependency Injection
There are several ways to inject dependencies into a class. The most common types are:
Constructor Injection: Dependencies are passed through the constructor.
Setter Injection: Dependencies are passed through setter methods.
Method Injection: Dependencies are passed directly to the method that needs them.
Each type has different use cases, but constructor injection is usually the most common and recommended approach for required dependencies.
Constructor Injection
Constructor injection means passing dependencies through the class constructor. This is the most common form of Dependency Injection.
class ReportService
{
public function __construct(
private ReportRepository $repository,
private PdfExporter $exporter
) {
}
public function generate(int $reportId): string
{
$report = $this->repository->findById($reportId);
return $this->exporter->export($report);
}
}Constructor injection is useful when a dependency is required for the class to work. It makes the dependency explicit and ensures the object is created in a valid state.
Setter Injection
Setter injection means passing a dependency through a setter method after the object is created.
class NewsletterService
{
private ?LoggerInterface $logger = null;
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
public function send(string $email): void
{
// Send newsletter
if ($this->logger) {
$this->logger->info('Newsletter sent.');
}
}
}Setter injection can be useful for optional dependencies. However, it should be used carefully because the object may exist without the dependency being set.
Method Injection
Method injection means passing a dependency directly into the method that needs it.
class FileImportService
{
public function import(string $filePath, FileParserInterface $parser): array
{
return $parser->parse($filePath);
}
}Method injection is useful when a dependency is needed only for one specific method and does not need to be stored as part of the object state.
Dependency Injection and Testing
One of the biggest benefits of Dependency Injection is easier testing. When a class receives dependencies from outside, tests can pass fake or mock dependencies instead of real services.
For example, a real payment gateway may call an external API. During unit tests, developers should not call the real payment provider. They can inject a fake payment gateway instead.
class FakePaymentGateway implements PaymentGatewayInterface
{
public function charge(Order $order): bool
{
return true;
}
}The test can now use the fake implementation:
$checkout = new CheckoutService(new FakePaymentGateway());
$result = $checkout->checkout($order);This makes tests faster, safer, and more predictable.
Dependency Injection and Loose Coupling
Loose coupling means that classes do not depend heavily on specific concrete implementations. Dependency Injection supports loose coupling because dependencies are provided from outside.
When a class depends on an interface, it can work with many different implementations. This reduces the impact of change.
For example, changing from StripePaymentGateway to PayPalPaymentGateway does not require changing the CheckoutService if both classes implement PaymentGatewayInterface.
Dependency Injection and SOLID Principles
Dependency Injection is closely related to SOLID principles, especially the Dependency Inversion Principle.
The Dependency Inversion Principle says that high-level modules should not depend on low-level modules. Both should depend on abstractions. It also says that abstractions should not depend on details. Details should depend on abstractions.
Dependency Injection helps apply this principle by allowing high-level services to depend on interfaces instead of concrete classes.
Dependency Injection vs Dependency Inversion
Dependency Injection and Dependency Inversion are related, but they are not the same.
Dependency Inversion is a design principle. It says that code should depend on abstractions rather than concrete implementations.
Dependency Injection is a technique used to provide dependencies to a class from the outside. It is one practical way to apply Dependency Inversion.
In simple terms, Dependency Inversion is the idea, and Dependency Injection is one way to implement that idea.
Dependency Injection Container
A Dependency Injection container is a tool that automatically creates objects and injects their dependencies. Instead of manually creating every service and passing dependencies, the container resolves them automatically.
Modern frameworks use containers heavily. The container knows how to build services, which implementation should be used for an interface, and how dependencies should be wired together.
For example, if a controller needs UserService, and UserService needs UserRepository, the container can create the full object graph automatically.
Manual Dependency Injection Example
Without a container, dependencies can be wired manually:
$repository = new UserRepository();
$emailService = new EmailService();
$userService = new UserService($repository, $emailService);This is clear in small applications, but it becomes harder when many services depend on other services.
A DI container automates this process.
Dependency Injection in Laravel
Laravel has a powerful service container that manages Dependency Injection. Laravel can automatically inject dependencies into controllers, services, jobs, listeners, middleware, and commands.
For example, a Laravel controller can receive a service through its constructor:
class UserController
{
public function __construct(
private UserService $userService
) {
}
public function store(Request $request)
{
return $this->userService->create($request->all());
}
}Laravel resolves UserService automatically from the service container.
Binding Interfaces in Laravel
When a class depends on an interface, Laravel needs to know which implementation should be used. This is done through binding.
use App\Contracts\PaymentGatewayInterface;
use App\Services\StripePaymentGateway;
public function register(): void
{
$this->app->bind(
PaymentGatewayInterface::class,
StripePaymentGateway::class
);
}After this binding, any class that requires PaymentGatewayInterface will receive StripePaymentGateway unless the binding is changed.
Laravel Singleton Binding
Laravel can also bind a service as a singleton, meaning the same instance is reused.
$this->app->singleton(
SettingsRepository::class,
DatabaseSettingsRepository::class
);This provides shared instance behavior through the service container without forcing the class itself to implement the Singleton Pattern manually.
Dependency Injection in Symfony
Symfony also uses a Dependency Injection container. Services are defined and configured, and Symfony injects dependencies automatically through constructors or service configuration.
For example, a Symfony service may receive a repository and logger through its constructor:
class ReportService
{
public function __construct(
private ReportRepository $repository,
private LoggerInterface $logger
) {
}
}Symfony can autowire these dependencies when the services are registered correctly.
Autowiring
Autowiring is a feature where the framework automatically detects dependencies from type hints and injects the correct services.
For example, if a constructor requires LoggerInterface, the container can provide the configured logger service automatically.
Autowiring reduces manual configuration and makes Dependency Injection easier to use, especially in modern PHP frameworks.
Dependency Injection and Design Patterns
Dependency Injection works well with many design patterns. It is often used with Repository Pattern, Strategy Pattern, Adapter Pattern, Decorator Pattern, Command Pattern, and Observer Pattern.
For example, a service may receive a repository through dependency injection. A checkout service may receive a payment strategy. A notification service may receive an adapter for an external SMS provider. A command handler may receive repositories and services.
Dependency Injection makes these patterns more flexible because dependencies can be replaced without changing the class that uses them.
Dependency Injection and Repository Pattern
Repository Pattern often uses Dependency Injection. A service can depend on a repository interface instead of a concrete repository implementation.
class UserService
{
public function __construct(
private UserRepositoryInterface $users
) {
}
}This makes the service independent from Eloquent, Doctrine, PDO, or any specific data source.
Dependency Injection and Strategy Pattern
Strategy Pattern also works naturally with Dependency Injection. A context class can receive a strategy through the constructor.
class DiscountService
{
public function __construct(
private DiscountStrategy $strategy
) {
}
public function calculate(float $total): float
{
return $this->strategy->calculate($total);
}
}The strategy can be changed without modifying the DiscountService.
Dependency Injection and Decorator Pattern
Decorator Pattern often uses Dependency Injection because a decorator wraps another object that implements the same interface.
class CachedProductRepository implements ProductRepositoryInterface
{
public function __construct(
private ProductRepositoryInterface $repository,
private CacheInterface $cache
) {
}
}The decorated repository and cache service are injected from outside, making the decorator flexible and testable.
Dependency Injection vs Service Locator
Service Locator is another pattern where a class asks a central object or container for its dependencies. For example, a class may call app(PaymentGateway::class) internally.
Dependency Injection is usually preferred because dependencies are visible in the constructor or method signature. This makes the class easier to understand and test.
Service Locator can hide dependencies. A class may appear to have no dependencies, but internally it fetches many services from the container. This makes the code harder to analyze.
Bad Example: Hidden Dependencies
class CheckoutService
{
public function checkout(Order $order): bool
{
$paymentGateway = app(PaymentGatewayInterface::class);
return $paymentGateway->charge($order);
}
}This hides the dependency inside the method. It is better to inject the payment gateway through the constructor.
Better Example: Explicit Dependencies
class CheckoutService
{
public function __construct(
private PaymentGatewayInterface $paymentGateway
) {
}
public function checkout(Order $order): bool
{
return $this->paymentGateway->charge($order);
}
}Now the dependency is explicit and easier to replace in tests.
Benefits of Dependency Injection
Dependency Injection provides many benefits in object-oriented software design.
Main benefits include:
Reduces tight coupling between classes.
Makes dependencies explicit.
Improves testability with fake or mock dependencies.
Supports interface-based design.
Makes code easier to extend and maintain.
Works well with SOLID principles.
Allows easier replacement of implementations.
Improves project organization in large applications.
These benefits make Dependency Injection one of the most important techniques in professional software development.
Drawbacks of Dependency Injection
Dependency Injection can also have drawbacks if it is used incorrectly. It may add more classes, interfaces, and configuration to the project.
For small applications, manual object creation may be simpler. In large applications, Dependency Injection is usually worth the structure.
Another drawback is constructor over-injection. If a class has too many injected dependencies, it may be a sign that the class has too many responsibilities and should be split into smaller classes.
Constructor Over-Injection
Constructor over-injection happens when a class requires too many dependencies in its constructor.
class ReportService
{
public function __construct(
private A $a,
private B $b,
private C $c,
private D $d,
private E $e,
private F $f
) {
}
}This may indicate that ReportService is doing too much. The solution is not to hide dependencies using a service locator. The better solution is to review the class responsibilities and split the logic if needed.
When to Use Dependency Injection
Use Dependency Injection when a class depends on services, repositories, external APIs, strategies, adapters, loggers, or other objects that may change or need testing.
Dependency Injection is useful when:
A class depends on another service or object.
You want to test a class with fake dependencies.
You want to depend on interfaces instead of concrete classes.
The implementation may change in the future.
The project uses a framework service container.
You want to reduce tight coupling.
You want dependencies to be visible and explicit.
If these conditions exist, Dependency Injection is usually a strong design choice.
When Not to Use Dependency Injection
Dependency Injection is not always necessary for every small object. Simple value objects, DTOs, entities, and small utility objects may not need injected services.
Avoid unnecessary Dependency Injection when:
The class has no real external dependencies.
The object is a simple data container or value object.
Manual creation is simpler and clearer.
The dependency will never be replaced or mocked.
The pattern adds complexity without improving design.
Good design should be practical. Dependency Injection should make the code clearer, not more complicated.
Common Mistakes with Dependency Injection
One common mistake is injecting too many dependencies into one class. This often means the class has too many responsibilities.
Another mistake is depending on concrete classes when an interface would be better. If the implementation may change, an interface can improve flexibility.
A third mistake is using the service container directly inside business classes. This hides dependencies and makes testing harder.
A fourth mistake is creating interfaces for every class without a real reason. Interfaces are useful when multiple implementations exist or when testing and decoupling matter.
Best Practices for Dependency Injection
To use Dependency Injection effectively, developers should keep dependencies clear, meaningful, and focused.
Useful best practices include:
Prefer constructor injection for required dependencies.
Use interfaces when multiple implementations may exist.
Keep dependencies explicit in the constructor.
Avoid using the service container as a hidden service locator.
Watch for constructor over-injection as a design warning.
Use setter injection only for optional dependencies.
Use method injection for dependencies needed by one method only.
Keep classes focused on one responsibility.
Use framework containers for service wiring in large applications.
These practices help keep Dependency Injection clean and useful.
Practical Checklist Before Using Dependency Injection
Before using Dependency Injection, developers can ask these questions:
Does this class depend on another service or object?
Will I need to replace this dependency in tests?
Could this dependency have multiple implementations?
Should this class know how to create the dependency?
Will injecting the dependency reduce coupling?
Is the dependency required or optional?
Does the constructor show too many responsibilities?
If the answer is yes to several of these questions, Dependency Injection is likely a good design choice.
Conclusion
Dependency Injection is a powerful technique in Object-Oriented Programming that allows classes to receive their dependencies from outside instead of creating them internally. It reduces tight coupling, makes dependencies explicit, improves testability, and supports clean software architecture.
Dependency Injection works especially well with interfaces, repositories, strategies, adapters, decorators, command handlers, and service layers. It is a core concept in modern frameworks such as Laravel and Symfony.
However, Dependency Injection should be used thoughtfully. Too many dependencies can reveal poor class design, and unnecessary abstractions can make simple code more complex. When applied correctly, Dependency Injection is one of the most important practices for writing clean, maintainable, and scalable object-oriented software.

