skip to content

In Laravel's service container, how do bind(), singleton(), scoped() and instance() differ in what repeated resolutions return?

level: middleimportance: must knowfreq 72%

answer

  1. one shared flag per binding
  2. bind: a new object per make()
  3. singleton: built lazily, then cached
  4. scoped: dropped by forgetScopedInstances()
  5. instance: your pre-built object, stored now

basics

~20 s

bind() builds a new object on every resolution; singleton() builds once, lazily, and returns that object afterwards; scoped() is a singleton forgotten when a queue job or Octane request starts; instance() stores an object you already built.

solid answer

~40 s

All four register into the same container. `bind()` stores a non-shared binding, so each `make()` runs the closure or builds the class again. `singleton()` is `bind()` with the shared flag set: nothing is built at registration, the first resolution builds the object and later ones get the cached copy. `scoped()` is a singleton whose key is also listed as scoped; `forgetScopedInstances()` drops those copies, and the queue worker calls it before fetching each job while Octane does so after each request. `instance()` takes an object you already built and stores it immediately. Under PHP-FPM each request boots a fresh application, so a singleton lives one request anyway. The `bindIf()`, `singletonIf()` and `scopedIf()` variants register only when the type is not already bound.

code

php · 11 lines
php
<?php

use App\Services\QuoteBuilder;
use App\Services\RateTableClient;
use Illuminate\Support\Facades\App;

App::bind(QuoteBuilder::class);
App::singleton(RateTableClient::class);

var_dump(app(QuoteBuilder::class) === app(QuoteBuilder::class));       // bool(false)
var_dump(app(RateTableClient::class) === app(RateTableClient::class)); // bool(true)

go deeper

for a junior

Recall the four calls and what a second resolution returns for each: a new object, the cached object, the cached object until the scope resets, or your own object.

for a middle

Explain the shared flag, lazy versus eager construction, which processes flush scoped instances, and why the If variants exist for packages.

for a senior

Choose lifetimes by state: stateless clients as singletons, per-request or per-job state as scoped, and recognise when a singleton will carry state from one queued job to the next.

for a principal

Set a team default for lifetimes that stays correct if the app later moves to long-lived workers, and decide which services must be reviewed before that move.

## One registry, one shared flag Laravel's container keeps a table of **bindings**: for each **abstract** (a class name, interface name or string key) it stores a **concrete** (a closure or class name) and a boolean **shared** flag. It also keeps a table of **instances**, the objects it has cached. The registration methods differ only in how they fill those two tables: - `bind($abstract, $concrete = null, $shared = false)` writes a binding with `shared = false`. - `singleton($abstract, $concrete = null)` calls `bind()` with `shared = true`. - `scoped($abstract, $concrete = null)` records the abstract in a list of **scoped** keys, then calls `singleton()`. - `instance($abstract, $object)` skips the bindings table and writes the object straight into the instances table. When you call `make()`, the container first looks for a cached instance; if there is none it builds the concrete, and it caches the result only when the binding is shared. ## The four calls side by side | Method | Object built | Repeated `make()` returns | Typical use | |---|---|---|---| | `bind()` | On every resolution | A new object each time | Stateful or cheap objects, per-use builders | | `singleton()` | On first resolution | The same object | Clients, configured services, expensive setup | | `scoped()` | On first resolution in a lifecycle | The same object until the scope is flushed | State tied to one request or one job | | `instance()` | Before registration, by you | The object you passed | Pre-configured objects, test doubles | If you omit the concrete, the abstract is bound to itself and auto-wired. If you pass **only a closure** with a declared return type, `bind()` reads the return type and registers the binding under that class. ## How scoped() differs from singleton() The difference only shows up when one PHP process handles more than one unit of work: 1. **PHP-FPM**: every HTTP request bootstraps a new application, so singletons and scoped instances both die with the request. They behave identically. 2. **Queue worker** (`queue:work`): one process runs many jobs. The worker's reset step calls `forgetScopedInstances()` before it fetches each job, so a scoped service starts fresh per job while a singleton carries over from job to job. 3. **Octane**: one worker serves many requests. Octane flushes scoped instances after each request; singletons resolved earlier persist. So `scoped()` is the choice for something that must be shared within one request or job but never leak into the next, such as a per-request tenant resolver. ## Conditional registration `bindIf()`, `singletonIf()` and `scopedIf()` first call `bound()`, which is true when the abstract has a binding, a cached instance or is an alias. If it is already bound they do nothing. Packages use them so an application that registered its own implementation first is not overwritten. ## Re-registering and other gotchas - Calling `bind()` or `singleton()` again for an abstract **drops the cached instance**, so the next resolution uses the new binding. If the abstract had already been resolved, any `rebinding()` callbacks run. - `singleton()` is **lazy**: registering it in a provider builds nothing. Only `instance()` is eager, because you built the object yourself. - An `extend()` decorator runs on each resolution for a non-shared binding but once for a shared one, since the decorated object is what gets cached. - The attribute forms of these lifetimes (`#[Singleton]`, `#[Scoped]`, `#[Bind]`) live on the class or interface itself; they are an alternative to provider calls, not a different lifetime. ## Choosing a lifetime A simple rule of thumb covers most services: - **Stateless and cheap**: either works; `bind()` is the honest default because nothing needs sharing. - **Stateless but expensive to build** (an HTTP client with configured base URLs, a parsed rate table): `singleton()`. - **Holds state for one unit of work** (the current tenant, a per-request cache of lookups): `scoped()`, so a queue worker or Octane does not carry it into the next job or request. - **Already built elsewhere**, or a hand-made test double: `instance()`. The mistake to avoid is a `singleton()` that stores something about the current request or job. Under PHP-FPM it looks correct, because the application is rebuilt for every request, and it starts misbehaving once the same code runs in a queue worker or under Octane. ```php $this->app->bind(QuoteBuilder::class); // new object each time $this->app->singleton(RateTableClient::class); // one object, built on first use $this->app->scoped(TenantContext::class); // one per request or job $this->app->instance(FuelSurcharge::class, new FuelSurcharge(0.12)); // stored now ```

  • In Laravel, a singleton was already resolved and a provider then calls bind() for the same abstract; what happens to the cached object?
    `bind()` drops the stale cached instance for that abstract, so the next resolution builds from the new binding. Because the abstract had been resolved, the container also fires any `rebinding()` callbacks registered for it, passing them a freshly resolved object. Objects that already captured the old instance keep it.
  • Under PHP-FPM, does a Laravel singleton() binding keep its object between two HTTP requests?
    No. Each FPM request bootstraps a new application and container, so a singleton lives for that request only. Cross-unit sharing appears when one process handles many units of work, as a queue worker does with jobs or Octane does with requests, which is where `scoped()` earns its place.
  • In Laravel, how can bind() register a type without naming the abstract?
    Pass only a closure with a declared return type, for example `App::bind(function (Application $app): QuoteBuilder { ... })`. The container reads the closure's return type and registers the binding under that class; a union return type registers it under each class in the union.

bind() is a bakery that bakes a fresh loaf for every customer; singleton() bakes one loaf at the first order and slices it for everyone after; scoped() bakes one shared loaf per shift and bins it when the shift ends; instance() is a loaf you brought from home and left on the counter.

saying these in an interview costs you the question

  • singleton() builds the object as soon as the provider registers it.
  • A singleton binding is shared across all HTTP requests under PHP-FPM.
  • scoped() gives each class that injects the service its own instance.
  • instance() takes a class name and builds the object lazily.
  • singletonIf() replaces whatever binding already exists for the type.