Laravel Service Container for Dependency Injection
TL;DR — Laravel’s container resolves dependencies automatically via constructor injection. Bind interface → impl in service providers. Use
singleton()for stateful services,bind()for transient. Contextual binding for when the same interface has different impls per consumer.
After Laravel + Clean Architecture layout, the actual mechanics of wiring it up. Laravel’s container is what lets you type-hint interfaces and get implementations back. Used right, it’s invisible. Used wrong, it’s magic that hides bugs.
How auto-resolution works
class StartSubscription
{
public function __construct(
private SubscriptionRepository $subs,
private PlanRepository $plans,
) {}
}
When Laravel needs a StartSubscription (via a route controller, a queue handler, etc.), it reads the constructor, sees the type hints, and resolves each from the container. The container looks up bindings; if none exists, it tries to auto-construct.
For concrete classes with no bindings: container news them up. For interfaces: you must register a binding.
Bindings in service providers
namespace App\Providers;
use App\Repository\SubscriptionRepository;
use App\Repository\PlanRepository;
use App\Repository\Eloquent\EloquentSubscriptionRepository;
use App\Repository\Eloquent\EloquentPlanRepository;
use Illuminate\Support\ServiceProvider;
class DomainServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(SubscriptionRepository::class, EloquentSubscriptionRepository::class);
$this->app->bind(PlanRepository::class, EloquentPlanRepository::class);
}
}
Add to config/app.php:
'providers' => [
// ...
App\Providers\DomainServiceProvider::class,
],
Now every class type-hinting SubscriptionRepository gets EloquentSubscriptionRepository.
bind vs singleton
// Transient: new instance each time
$this->app->bind(SubscriptionRepository::class, EloquentSubscriptionRepository::class);
// Singleton: same instance per request
$this->app->singleton(StripeClient::class, function ($app) {
return new StripeClient(config('services.stripe.secret'));
});
When to use each:
bind: stateless services (repositories, use cases). Fresh per use.singleton: services with expensive setup (HTTP clients with connection pools, Redis clients). Created once, reused.
In Laravel, “per request” is the lifetime — the container is rebuilt on every HTTP request. Singletons last for one request, not across the process. So singletons are essentially free; they’re a perf optimization, not a state-sharing mechanism.
Closure bindings for parameterized services
$this->app->singleton(StripeClient::class, function ($app) {
return new StripeClient(
secretKey: config('services.stripe.secret'),
timeout: config('services.stripe.timeout', 5),
);
});
When the implementation needs config, env, or other runtime values, use a closure. The closure runs lazily — only when the binding is first resolved per request.
Contextual binding
Sometimes the same interface needs different implementations depending on who’s asking:
$this->app->when(StartSubscription::class)
->needs(EventPublisher::class)
->give(KafkaEventPublisher::class);
$this->app->when(StandupBot::class)
->needs(EventPublisher::class)
->give(SlackEventPublisher::class);
Now StartSubscription gets a Kafka publisher; StandupBot gets a Slack publisher. Same interface, different impl per consumer.
Use sparingly. Often a sign that the interface is too generic.
Resolving via container directly
For cases where you can’t type-hint (eg, inside a closure or static context):
$startSub = app(StartSubscription::class);
$result = $startSub->execute($input);
// or with explicit container
$startSub = app()->make(StartSubscription::class);
app() is the global container helper. Avoid in user code; use constructor injection where possible. Useful in tests and in some framework hooks.
Testing — overriding bindings
public function test_start_subscription_publishes_event()
{
$this->app->bind(SubscriptionRepository::class, fn() => new InMemorySubscriptionRepository());
$this->app->bind(PlanRepository::class, fn() => new InMemoryPlanRepository());
$events = $this->getMockBuilder(Dispatcher::class)->getMock();
$events->expects($this->once())->method('dispatch');
$this->app->bind(Dispatcher::class, fn() => $events);
$useCase = $this->app->make(StartSubscription::class);
$useCase->execute(new StartSubscriptionInput(
customerId: Uuid::uuid4(),
planId: 'pro_monthly',
));
}
Per-test bindings override the production ones. Use cases get the test doubles automatically.
For most tests, hand-written fakes (the InMemory*Repository classes) are clearer than mock libraries. Mocks are heavier; fakes hold state.
What NOT to put in the container
- Configuration objects. Use
config()directly. - Single-use values. Pass as method args.
- Request-specific state. Use the Request object.
- Eloquent models. Construct directly; the container isn’t useful for them.
The container is for services with non-trivial dependencies. Not a global hashmap.
A complete provider
namespace App\Providers;
use App\Repository\PlanRepository;
use App\Repository\SubscriptionRepository;
use App\Repository\Eloquent\EloquentPlanRepository;
use App\Repository\Eloquent\EloquentSubscriptionRepository;
use App\Service\Billing\StripeClient;
use Illuminate\Support\ServiceProvider;
class DomainServiceProvider extends ServiceProvider
{
public function register(): void
{
// Repositories
$this->app->bind(SubscriptionRepository::class, EloquentSubscriptionRepository::class);
$this->app->bind(PlanRepository::class, EloquentPlanRepository::class);
// Services
$this->app->singleton(StripeClient::class, function () {
return new StripeClient(
secretKey: config('services.stripe.secret'),
timeout: 10,
);
});
}
public function boot(): void
{
// Runs after all providers registered. Use for things needing other providers to be ready.
}
}
Bindings in register(). Things that depend on bound services going boot().
Common Pitfalls
new EloquentSubscriptionRepository() inside a use case. Bypasses DI. Now you can’t swap for tests. Type-hint the interface; let the container resolve.
app() calls scattered everywhere. Defeats DI. Use type hints in constructors.
Singletons holding request-specific state. Container is per-request anyway, but singletons across requests in a long-lived worker (like Laravel Octane) can leak state.
Auto-discovery of providers in non-Laravel-managed code. Stick to Laravel’s lifecycle. Don’t try to wire providers manually.
Binding interface → interface. Doesn’t help. Bind interface → concrete.
Circular dependencies. A → B → A. Container will fail at resolution time. Refactor.
Wrapping Up
Service container for DI: bind interface to impl in providers; type-hint constructors; override in tests. Friday: Eloquent vs domain models — the most common Laravel-specific Clean Architecture friction.