0

Service provider boot order and the deferred provider system

Advanced5 min read·lv-01-006
laravel-srcinterview

Concept

Service providers are the backbone of Laravel's bootstrap, and their execution order is not arbitrary — it has a carefully designed two-phase architecture. The deferred provider system adds a third phase that allows bindings to be lazy-loaded only when actually needed. Together, these mechanisms give Laravel both predictability and performance.

The boot order for all providers is: first every provider's register() is called, then every provider's boot() is called. This two-phase design exists for a critical reason: register() must not assume that other services are available. If UserServiceProvider::register() tried to resolve Cache to configure something, and CacheServiceProvider::register() hadn't run yet, you'd get a BindingResolutionException. By separating registration from booting, Laravel guarantees that when boot() runs, all bindings exist.

Within each phase, the order follows the providers array in config/app.php plus any packages discovered via Composer's extra.laravel.providers mechanism. Framework providers registered in registerBaseServiceProviders() (EventServiceProvider, LogServiceProvider, RoutingServiceProvider) always run first, before any user-defined providers.

The deferred provider system works through a manifest file at bootstrap/cache/services.php. To defer a provider, it must implement Illuminate\Contracts\Support\DeferrableProvider and define a provides() method returning an array of abstract binding names. During RegisterProviders, the bootstrapper checks if a provider is deferrable; if so, it skips instantiation and writes the provider class name and its bindings to the manifest. The Application::make() method checks the manifest before attempting to resolve — if the abstract is found in the manifest, the responsible provider is loaded and registered on-demand.

The manifest is regenerated by php artisan clear-compiled or when vendor:publish modifies provider configurations. In production, the --optimize flag of artisan optimize ensures the manifest is pre-built.

A subtle timing issue: deferred providers can only register bindings into the container (in register()). They cannot register routes, add middleware, or hook into events during their deferred load — those happen in boot(), but by the time a deferred provider's boot() runs, the HTTP Kernel's routing and middleware setup is complete.

Code Example

php
<?php
// Creating a deferred service provider
namespace App\Providers;

use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider;
use App\Services\HeavyReportingService;
use App\Services\PdfGenerator;

class ReportingServiceProvider extends ServiceProvider implements DeferrableProvider
{
    public function register(): void
    {
        // These bindings are registered LAZILY — only when HeavyReportingService
        // or PdfGenerator is first resolved from the container
        $this->app->singleton(HeavyReportingService::class, function ($app) {
            return new HeavyReportingService(
                $app->make('db'),
                config('reporting.cache_ttl')
            );
        });

        $this->app->bind(PdfGenerator::class, function ($app) {
            return new PdfGenerator(config('reporting.template_path'));
        });
    }

    public function boot(): void
    {
        // boot() runs when the provider is loaded — which may be mid-request
        // Avoid route/middleware registration here for deferred providers
    }

    // REQUIRED: tells the manifest what this provider binds
    // Must match EXACTLY what register() binds
    public function provides(): array
    {
        return [
            HeavyReportingService::class,
            PdfGenerator::class,
        ];
    }
}
php
<?php
// config/app.php — controlling boot order

'providers' => [
    // Framework providers — always first, hardcoded in Application::registerBaseServiceProviders()
    // Illuminate\Events\EventServiceProvider — implicitly first
    // Illuminate\Log\LogServiceProvider — second
    // Illuminate\Routing\RoutingServiceProvider — third

    // Then these run in order:
    Illuminate\Auth\AuthServiceProvider::class,
    Illuminate\Cache\CacheServiceProvider::class,    // deferred
    Illuminate\Database\DatabaseServiceProvider::class,
    Illuminate\Queue\QueueServiceProvider::class,    // deferred
    // ...

    // Your providers run last — they can depend on everything above
    App\Providers\AppServiceProvider::class,
    App\Providers\AuthServiceProvider::class,
    App\Providers\EventServiceProvider::class,
    App\Providers\RouteServiceProvider::class,
    App\Providers\ReportingServiceProvider::class,   // deferred
],
php
<?php
// Inspecting the deferred manifest at runtime
// bootstrap/cache/services.php content (example):
return [
    'providers' => [
        'Illuminate\Cache\CacheServiceProvider',
        'Illuminate\Queue\QueueServiceProvider',
        'App\Providers\ReportingServiceProvider',
    ],
    'eager' => [...],
    'deferred' => [
        'cache' => 'Illuminate\Cache\CacheServiceProvider',
        'cache.store' => 'Illuminate\Cache\CacheServiceProvider',
        'queue' => 'Illuminate\Queue\QueueServiceProvider',
        'App\Services\HeavyReportingService' => 'App\Providers\ReportingServiceProvider',
    ],
    'when' => [],
];

Interview Q&A

Q: Why does Laravel separate register() and boot() into two distinct phases instead of using a single method?

The two-phase design solves the provider dependency ordering problem. During register(), providers must only bind things into the container — they cannot assume other services exist because other providers may not have registered yet. If all registration and setup happened in one method, every provider's setup order would matter, and circular dependencies (Provider A uses Provider B's bindings, but B calls A's bindings) would be unavoidable. By forcing all register() calls to complete before any boot() call, Laravel guarantees that in boot(), the full container is available. This is why event listeners, route macros, and Blade directives go in boot() — they can safely depend on other services being registered.


Q: How does a deferred provider differ from an eager provider in terms of what gets stored in memory?

An eager provider is instantiated (new SomeServiceProvider($app)) and its register() method is called during the RegisterProviders bootstrapper. Its bindings are added to the container's $bindings array immediately. A deferred provider is never instantiated during bootstrap. Instead, only two strings (the provider class name and the binding name it provides) are stored in the deferred services manifest file. No PHP object is created, no bindings are added to the container. When application code first calls app(HeavyService::class), the container checks the manifest, loads the provider class, instantiates it, calls register(), and then resolves the binding. This means a complex provider with expensive register() logic costs nothing in memory or time if nothing ever resolves its bindings.


Q: What happens if a deferred provider's provides() array doesn't match what it actually binds in register()?

The manifest will be incomplete or incorrect. If provides() omits a binding that register() actually binds, that binding will not be lazy-loaded — the provider will never be triggered for that abstract, and app(MissingClass::class) will throw BindingResolutionException. If provides() includes bindings that register() doesn't actually bind, those entries are dead entries in the manifest that waste a lookup on every container resolution for those abstracts. The most dangerous scenario: a developer adds a new binding to register() but forgets to add it to provides() — this works fine locally (the provider loads for other bindings), but in production with php artisan optimize, the manifest is stale and the new binding silently fails to resolve.