Binding — bind(), singleton(), instance(), scoped()
Concept
Binding is the act of telling the container how to build a class or what instance to return when a given abstract is requested. Laravel's container provides four primary binding methods — bind(), singleton(), instance(), and scoped() — each with a different lifetime model.
bind(abstract, concrete) registers a transient binding. Every call to app()->make(abstract) creates a fresh instance. The concrete can be a class name string (the container will auto-wire it) or a Closure that receives the container and returns an instance. Use bind() for stateless services where sharing state between calls would be wrong — form request objects, DTOs, command handlers.
singleton(abstract, concrete) registers a singleton binding. The first resolution creates the instance; every subsequent call to app()->make(abstract) returns the same object. This is appropriate for services that maintain state between uses within a single request: the database connection manager, cache repositories, the mailer, the queue manager. In PHP-FPM, the singleton lives for one request; in Octane (long-running processes), it lives for the entire process lifetime.
instance(abstract, instance) binds a pre-existing object to an abstract. The container will return this exact object every time. No factory closure is involved — you've already created the object. This is heavily used in testing: $this->app->instance(MailerInterface::class, $fakeMail) makes the container return your fake without creating a new one.
scoped(abstract, concrete) is like singleton(), but the instance is cleared at the end of each request/job lifecycle. This is specifically for Laravel Octane, where workers handle multiple requests in one process. A singleton() would persist its instance across all requests (potential data leakage between users). A scoped() binding is reset between requests by calling app()->forgetScopedInstances() in the ScopedMiddleware. Use scoped() for services that should be singletons per-request (like the authenticated user cache) but must not bleed into the next request.
There is also bindIf(), singletonIf(), and scopedIf() — conditional variants that only register if the abstract has not already been bound. These are used by package service providers that don't want to override user-defined bindings.
Code Example
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// 1. bind() — fresh instance every time make() is called
$this->app->bind(
\App\Contracts\PaymentGatewayInterface::class,
\App\Services\StripePaymentGateway::class
);
// 2. bind() with a closure — when construction needs runtime config
$this->app->bind(\App\Services\PdfRenderer::class, function ($app) {
return new \App\Services\PdfRenderer(
config('pdf.engine'),
$app->make(\App\Contracts\FontManagerInterface::class)
);
});
// 3. singleton() — same instance for the lifetime of the process/request
$this->app->singleton(\App\Services\ReportCache::class, function ($app) {
return new \App\Services\ReportCache(
$app->make('cache')->store('redis'),
config('reports.ttl')
);
});
// 4. instance() — bind a pre-constructed object
$config = new \App\ValueObjects\AppConfiguration(
apiKey: config('services.external.key'),
timeout: 30,
);
$this->app->instance(\App\ValueObjects\AppConfiguration::class, $config);
// 5. scoped() — singleton per Octane request, reset between requests
$this->app->scoped(\App\Services\RequestContextService::class, function ($app) {
return new \App\Services\RequestContextService;
});
// 6. Conditional binding — won't overwrite if already bound
$this->app->singletonIf(
\App\Contracts\LogFormatterInterface::class,
\App\Services\JsonLogFormatter::class
);
}
}<?php
// Understanding the difference in practice
// bind() — each resolution creates new instance
$a = app(\App\Contracts\PaymentGatewayInterface::class);
$b = app(\App\Contracts\PaymentGatewayInterface::class);
$a === $b; // false — different objects
// singleton() — same instance returned every time
$c = app(\App\Services\ReportCache::class);
$d = app(\App\Services\ReportCache::class);
$c === $d; // true — identical object reference
// instance() — the exact object you provided
$cfg = app(\App\ValueObjects\AppConfiguration::class);
// Returns the exact $config object created above
// Binding a concrete class directly (no interface mapping)
// Container will auto-wire dependencies via Reflection
$this->app->singleton(\App\Services\UserAuditService::class);
// No closure needed — container reads the constructor and resolves depsInterview Q&A
Q: What is the difference between bind() and singleton() in the Laravel service container?
bind() registers a transient factory — every call to make() creates a new instance. singleton() registers a shared factory — the first call creates the instance, caches it in $this->instances, and every subsequent call returns the same object. The choice depends on whether the service maintains mutable state that should be isolated per use. Stateless services (pure functions, command handlers) should use bind(). Stateful services (connection pools, caches, session handlers) should use singleton(). Using singleton() for a service that holds per-request mutable state is a subtle Octane bug: in long-running processes, one user's state bleeds into the next user's request.
Q: When would you use instance() instead of singleton()?
instance() is for binding a pre-constructed object — you've already created it and just want the container to return it. singleton() is for lazy construction — the object is created on first resolution. Use instance() in tests when you want to control the exact object the container returns: $this->app->instance(MailerInterface::class, new FakeMail). Use singleton() in service providers for objects whose construction requires runtime config (config values, other services) that isn't available until the provider runs. There's also a performance angle: instance() is slightly faster because the container doesn't need to invoke a factory closure — it just stores and returns the object reference directly.
Q: What is scoped() and why was it introduced? Why doesn't singleton() work correctly for Octane applications?
singleton() in a PHP-FPM application is effectively "singleton per request" because the PHP process is reset after each request. But Laravel Octane keeps PHP workers alive across thousands of requests. A singleton() binding persists its instance across all those requests — if the service stores the authenticated user or request-specific context, it will serve wrong data to subsequent users. scoped() was introduced to be "singleton per Octane request" (or per queue job). The Octane worker calls app()->forgetScopedInstances() after each request, clearing all scoped instances. For $this->app->scoped(AuthenticatedUserCache::class), each new request gets a fresh instance, but within a single request it is shared. Normal PHP-FPM applications don't need scoped() (regular singleton() is already per-request), but it's good practice to use scoped() for request-bound services so your code is Octane-ready.