Learn how to implement the DeferrableProvider interface for lazy-loading services. A subtle but impactful feature for optimizing package performance.
Service providers registered at Laravel’s boot time are loaded on every request — even if that service is never used during a given request. Deferred Service Providers solve this problem by deferring the loading of a provider until its service is actually needed.
This page assumes familiarity with Laravel Package Development. Make sure you understand the fundamentals of service providers before reading on.
A typical service provider runs register() and boot() on every request. Initializing mail, queues, cache, and other features that are not needed on every page is wasteful.
// This provider's register() is called on every requestclass ReportServiceProvider extends ServiceProvider{ public function register(): void { // Binds a heavy reporting service every time $this->app->singleton(ReportGenerator::class, function ($app) { return new ReportGenerator( $app->make(PdfRenderer::class), $app->make(ChartRenderer::class), $app->make(DataExporter::class), ); }); }}
By deferring this provider, initialization only happens on requests that actually generate a report.
Implementing a deferred provider takes three steps.
1
Implement DeferrableProvider
use Illuminate\Contracts\Support\DeferrableProvider;use Illuminate\Support\ServiceProvider;class ReportServiceProvider extends ServiceProvider implements DeferrableProvider{ // ...}
2
Register bindings in register()
Write your bindings in register() just like a regular provider.
public function register(): void{ $this->app->singleton(ReportGenerator::class, function ($app) { return new ReportGenerator( $app->make(PdfRenderer::class), $app->make(ChartRenderer::class), $app->make(DataExporter::class), ); }); $this->app->singleton(ReportRepository::class, function ($app) { return new ReportRepository($app->make('db')); });}
3
Return registered services from provides()
provides() must return every service bound in register(). Laravel uses this list to determine which provider to load when a given service is requested.
public function provides(): array{ return [ ReportGenerator::class, ReportRepository::class, ];}
At boot time, Laravel generates a manifest file at bootstrap/cache/services.php. This file stores a mapping of all services provided by deferred providers.
// bootstrap/cache/services.php (auto-generated)return [ 'providers' => [ // same list as bootstrap/providers.php ], 'eager' => [ // eagerly loaded providers App\Providers\AppServiceProvider::class, ], 'deferred' => [ // service name => provider class mappings 'App\Services\ReportGenerator' => ReportServiceProvider::class, 'App\Repositories\ReportRepository' => ReportServiceProvider::class, 'cache' => Illuminate\Cache\CacheServiceProvider::class, 'cache.store' => Illuminate\Cache\CacheServiceProvider::class, 'queue' => Illuminate\Queue\QueueServiceProvider::class, ], 'when' => [],];
Thanks to this manifest, Laravel knows which provider supplies which service without loading any provider files. The actual provider is only loaded the first time its service is resolved.
After adding or modifying providers, regenerate the manifest:
If a binding is missing from provides(), that service will never be resolved.
public function register(): void{ $this->app->singleton(ReportGenerator::class, fn ($app) => new ReportGenerator()); // Added binding $this->app->singleton('report', fn ($app) => $app->make(ReportGenerator::class));}public function provides(): array{ return [ ReportGenerator::class, 'report', // ← string-keyed bindings must be listed too ];}
The same applies when using the $bindings / $singletons properties — every key must be included in provides().
class AnalyticsServiceProvider extends ServiceProvider implements DeferrableProvider{ public $singletons = [ AnalyticsClient::class => DefaultAnalyticsClient::class, ]; public function provides(): array { // Return every key listed in $singletons return [ AnalyticsClient::class, ]; }}
Deferred providers are designed solely for registering container bindings. Providers that do the following in boot() cannot be deferred.
Constraint
Reason
Route registration
Routes must be resolved at application boot time
Global middleware registration
Must be registered before the request pipeline runs
Event listeners (always needed)
Must be registered before the event fires
Blade directive registration
Must be registered before views are compiled
You can write a boot() method in a deferred provider, but its contents will not run until the service is resolved. Placing “always needed” logic like route or middleware registration in boot() will lead to unexpected behavior.
The when() method lets you load a provider when a specific event fires, rather than only when a service is resolved. This is useful for providers that are only needed in certain contexts, such as job processing.
class ReportServiceProvider extends ServiceProvider implements DeferrableProvider{ public function register(): void { $this->app->singleton(ReportGenerator::class, fn () => new ReportGenerator()); } public function provides(): array { return [ReportGenerator::class]; } /** * Load this provider when the given events fire, * even if the service itself has not been resolved yet. */ public function when(): array { return [ \App\Events\ReportRequested::class, ]; }}
When any event listed in when() fires, the provider is loaded even if none of its services have been directly resolved.
namespace Acme\Analytics;use Illuminate\Contracts\Support\DeferrableProvider;use Illuminate\Support\ServiceProvider;class AnalyticsServiceProvider extends ServiceProvider implements DeferrableProvider{ public function register(): void { $this->mergeConfigFrom(__DIR__.'/../config/analytics.php', 'analytics'); $this->app->singleton(AnalyticsManager::class, function ($app) { return new AnalyticsManager($app->make('config')->get('analytics')); }); $this->app->singleton('analytics', fn ($app) => $app->make(AnalyticsManager::class)); } public function boot(): void { // Keep boot() limited to publish registration if ($this->app->runningInConsole()) { $this->publishes([ __DIR__.'/../config/analytics.php' => config_path('analytics.php'), ], 'analytics-config'); } } public function provides(): array { return [ AnalyticsManager::class, 'analytics', ]; }}
mergeConfigFrom() internally checks for a cached config, so it is safe to call inside a deferred provider’s register(). However, if config is already cached, it has no effect.
Separating Artisan command registration with runningInConsole()
Command registration is only needed when Artisan is running, so guard it with runningInConsole(). If you want to defer a provider that also registers commands, either include the command classes in provides() or create a separate provider for commands.
public function boot(): void{ if ($this->app->runningInConsole()) { $this->commands([ AnalyticsFlushCommand::class, ]); }}