Skip to main content
David Dew Mallick

David Dew Mallick

ProjectsExperienceSkillsBlog
Back to blog
laravel dependency injectionlaravel interface bindinglaravel contextual bindinglaravel service provider

Laravel Service Container: Bindings, Contextual Injection

Master the Laravel service container: bindings, singletons, contextual injection, and interface resolution to write testable Laravel code with zero guesswork.

Sep 13, 2026 · 6 min read

The Laravel service container is the object graph behind every controller, job, and command you write. When a class asks for a dependency in its constructor, the container decides what to build, when to reuse it, and which concrete class satisfies an interface. Get the bindings right and your code becomes testable and explicit; get them wrong and you get silent misresolution, duplicated connections, and tests that pass locally but fail in production. This guide covers binding types, interface resolution, contextual binding, and the debugging commands that tell you what the container actually resolved.

How does the Laravel service container resolve dependencies?

Resolution is recursive. When you call app(ReportGenerator::class), the container reflects on the constructor, resolves each type-hinted parameter in turn, and instantiates the class. Scalar parameters without defaults throw a BindingResolutionException, which is why config values should be injected as typed objects or read inside the class.

Two resolution paths exist: explicit bindings registered in a service provider, and zero-config auto-wiring for concrete classes. Auto-wiring is fine for stateless helpers. Anything that touches I/O, external APIs, or shared state should be bound explicitly so tests can swap it.

Resolution order

Laravel checks for an explicit binding first, then falls back to reflection-based auto-wiring. Interfaces have no auto-wiring path, so an unbound interface always fails at resolution time, not at boot time.

In production systems I've tuned, the most common container bug is an interface type-hint that was never bound in a provider. The failure surfaces only when that code path executes, which is why the php artisan about and container inspection commands matter during review.

Laravel dependency injection: bindings that actually matter

Four registration styles cover nearly every case. Choose deliberately, because each has different lifetime semantics.

MethodLifetimeUse when
bind()New instance per resolutionStateless services, cheap objects
singleton()One instance per request/processConnection pools, clients with handshakes
scoped()One instance per request or jobPer-request state under Octane
instance()Pre-built objectTests, config-derived values

scoped() is the correct choice for anything holding request state when running under Laravel Octane, because a plain singleton survives across requests in a long-lived worker and leaks state between users. That distinction is the difference between a stable Octane deployment and intermittent cross-request contamination.

namespace App\Providers;

use App\Services\ExchangeRateClient;
use App\Contracts\RateProvider;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(ExchangeRateClient::class, function ($app) {
            return new ExchangeRateClient(
                baseUrl: config('services.rates.url'),
                timeout: config('services.rates.timeout', 3),
            );
        });

        $this->app->scoped(RequestContext::class);
    }
}

Never resolve a singleton inside a constructor if it depends on the current request. Inject a factory closure or resolve it lazily with app() at call time, otherwise you capture the first request's state forever.

Laravel interface binding and testability

Type-hint interfaces, not concrete classes. This is the single change that makes a codebase testable without mocking internals.

namespace App\Contracts;

interface RateProvider
{
    public function rateFor(string $base, string $quote): float;
}

// In AppServiceProvider::register()
$this->app->bind(RateProvider::class, ExchangeRateClient::class);

Any class that type-hints RateProvider now receives the bound implementation. In a test, rebind it to a fake and the consumer never changes:

public function test_invoice_uses_provider_rate(): void
{
    $this->app->instance(RateProvider::class, new FakeRateProvider(rate: 110.5));

    $invoice = app(InvoiceBuilder::class)->build(orderId: 42);

    $this->assertSame(110.5, $invoice->rate);
}

Bind the interface in a dedicated provider rather than AppServiceProvider once you have more than a handful of bindings. A DomainServiceProvider keeps registration order readable and makes it obvious which module owns which contract. The Laravel container documentation covers the full binding API if you need the rarer variants.

Laravel contextual binding for shared interfaces

One interface, two implementations, different consumers. Contextual binding solves this without factories or conditionals at the call site.

$this->app->when(ReportExporter::class)
    ->needs(RateProvider::class)
    ->give(fn () => new CachedRateProvider(
        inner: new ExchangeRateClient(config('services.rates.url')),
        cache: app('cache.store'),
    ));

$this->app->when(InvoiceBuilder::class)
    ->needs(RateProvider::class)
    ->give(ExchangeRateClient::class);

Contextual bindings are evaluated before the global interface binding, so they take precedence for the listed consumers only. Keep the list short: once five classes need five different implementations, the interface is doing too much and should be split. Contextual binding is a precision tool, not an architecture.

The Laravel service provider is where wiring lives

Providers run in two phases. register() must only bind things into the container and must never resolve other services, because the provider you depend on may not have run yet. boot() runs after every provider has registered, so it is the correct place for event listeners, route model bindings, and anything that calls app().

PhaseSafe to doNever do
register()Bindings, aliases, singletonsResolve services, hit the database
boot()Listeners, macros, view composersHeavy I/O on every request

Deferred providers implement provides() and are only loaded when one of their bindings is requested. For a provider with several bindings that most requests never touch, deferring it removes boot cost from every request. Verify the effect with php artisan about rather than assuming.

Debugging what the container resolved

When resolution surprises you, inspect it instead of guessing. Tinker gives you the resolved class and its dependencies directly:

php artisan tinker
>>> app(App\Contracts\RateProvider::class)::class;
=> "App\Services\ExchangeRateClient"

>>> app()->bound(App\Contracts\RateProvider::class);
=> true

For a full dump of every registered binding, use php artisan about to confirm environment and cache state, then clear a stale compiled container with php artisan optimize:clear. A cached container from a previous deploy is a frequent cause of "my new binding is ignored" reports. For queue workers and Octane, remember that the container is built once per worker process, so restart workers after changing providers.

Add a container smoke test that resolves every interface your app binds. It fails fast in CI when someone adds a type-hint without a matching binding, instead of failing at 2am in a job.

FAQ

What is the difference between bind and singleton?

bind() returns a new instance on every resolution. singleton() builds once and returns the same instance for the rest of the request or worker lifetime. Use singletons only for objects that are safe to share.

Why does resolving an interface throw BindingResolutionException?

Interfaces cannot be auto-wired because PHP reflection cannot instantiate them. Register a binding from the interface to a concrete class in a service provider, then resolution succeeds.

Can I use contextual binding with closures?

Yes. give() accepts a class name, an instance, or a closure. Closures let you build the implementation with its own dependencies while still scoping it to one consumer.

Does the container cache bindings in production?

php artisan config:cache and route:cache do not cache container bindings, but compiled service manifests affect deferred providers. Run php artisan optimize:clear after changing provider registration.


Related reading: Laravel queue best practices for how container lifetimes interact with workers, and fixing Eloquent N+1 queries for the data layer these services wrap. More Laravel and PostgreSQL deep dives live on the blog index.

DD

David Dew Mallick

Software Engineer

I build AI-driven SaaS infrastructure and backend systems with Laravel, AWS, and SQL, and write about the engineering decisions behind them.

GitHubLinkedInEmail

More posts

  • Sep 22, 2026 · 8 min read

    Laravel Middleware: A Practical Guide to Request Flow

    Laravel middleware controls every request your app handles. Learn to write, register, and order middleware for auth, throttling, and request mutation.

  • Sep 19, 2026 · 8 min read

    Laravel Caching Strategies That Cut DB Load

    Laravel caching strategies that cut database load: Redis drivers, cache tags, atomic locks, and stampede protection with real code.

End of record

Back to top↑

David Dew Mallick

Dhaka, Bangladesh

Contact

  • david.dew.mallick@g.bracu.ac.bd
  • GitHub
  • LinkedIn

2026 David Dew Mallick