In a growing Laravel project, application code quickly starts working with databases, HTTP, cache, filesystems, and external processes.
The problem begins when business or application logic directly knows about a specific MySQL repository, Laravel HTTP client, or the ffprobe command.
In ICanUp, those details are hidden behind contracts in several important places. This is where Dependency Inversion Principle becomes practical.
Dependency Inversion Principle
Dependency Inversion Principle: High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
In simpler terms: important application logic should know what it needs, but it should not decide which concrete infrastructure technology must provide it.
Example 1 - Analytics Does Not Depend Directly on MySQL
interface AnalyticsAggregationRepositoryInterface{ public function aggregate( AnalyticsAggregationQuery $query, ): iterable;} final readonly class ContentAnalyticsQuery{ public function __construct( private AnalyticsAggregationRepositoryInterface $aggregationRepository, private AnalyticsPeriodResolver $periodResolver, ) {}}ContentAnalyticsQuery needs aggregated analytics data. It does not need to know which table stores that data, which SQL is executed, or whether caching is involved.
It therefore depends on AnalyticsAggregationRepositoryInterface. From the application layer point of view, the required capability is simply aggregate().
$this->app->bind( AnalyticsAggregationRepositoryInterface::class, static fn ( Application $app, ): CachedAnalyticsAggregationRepository => new CachedAnalyticsAggregationRepository( source: $app->make( MySqlAnalyticsAggregationRepository::class, ), cache: $app->make( AnalyticsAggregationCache::class, ), ),);The concrete composition happens in AnalyticServiceProvider: the contract resolves to a cached repository that internally uses MySqlAnalyticsAggregationRepository.
If persistence or caching changes, ContentAnalyticsQuery does not need to become a place full of MySQL or cache details.
The dependency points from application code toward the abstraction, while infrastructure adapts to that contract.
Example 2 - The IndexNow Job Does Not Know How HTTP Is Sent
interface IndexNowClientInterface{ public function submit(array $urls): void;} final class SubmitIndexNowUrlsJob implements ShouldQueue{ public function handle( IndexNowClientInterface $client, ): void { if (! (bool) config('indexnow.enabled')) { return; } $client->submit($this->urls); }SubmitIndexNowUrlsJob has a simple task: pass URLs for submission to IndexNow.
The job does not build the HTTP payload, read the endpoint, host, key, or timeout, and it does not call Http::post(). It depends on IndexNowClientInterface and only calls submit().
$this->app->bind( IndexNowClientInterface::class, IndexNowHttpClient::class,);The infrastructure implementation IndexNowHttpClient knows the HTTP details: configuration, host, key, endpoint, timeout, and the Laravel Http facade.
Laravel Service Container connects the contract to that implementation while the job remains independent of the concrete HTTP mechanism.
Example 3 - Media Validation Does Not Depend Directly on FFprobe
interface VideoMetadataReaderInterface{ public function read( UploadedFile $file, ): VideoMetadata;} final readonly class MediaUploadValidator{ public function __construct( private MediaTypeResolver $mediaTypeResolver, private MediaValidationConfigFactory $configFactory, private VideoMetadataReaderInterface $videoMetadataReader, ) {} private function validateVideo(MediaUploadValidator needs video width, height, and duration so it can apply validation rules.
It does not need to know that those values are currently produced by running the external ffprobe process. The validator therefore depends on VideoMetadataReaderInterface.
The concrete FFprobeVideoMetadataReader runs Symfony Process and converts its output into VideoMetadata. That is an infrastructure detail.
$this->app->bind( VideoMetadataReaderInterface::class, FFprobeVideoMetadataReader::class,);An Interface Alone Does Not Mean Dependency Inversion
For DIP, simply putting SomethingInterface in front of a concrete class is not enough.
I look at the dependency direction. If application code still creates new MySqlRepository(), calls Http::post(), or runs ffprobe directly, the abstraction has not really separated high-level logic from details.
In these examples, the contracts sit exactly at the boundary between application or domain needs and infrastructure implementations.
What Laravel Service Container Does Here
Service Container is not Dependency Inversion Principle by itself. It is a tool that helps compose dependencies in one place.
An application class asks for a contract. A Service Provider decides which concrete implementation will satisfy it. Infrastructure choices therefore do not spread across application code.
Practical Takeaway
My simplest formulation of Dependency Inversion Principle is: important logic should depend on the capability it needs, not on the concrete technology that provides that capability.
Analytics needs aggregated data, not MySQL. The IndexNow job needs URL submission, not the Laravel HTTP facade. Media validation needs video metadata, not FFprobe.
This keeps infrastructure replaceable or composable without pulling its details into code that solves the main application problem.



