📁 Volume II: Laravel Performance Tuning Kit

🎣 Topic 23: Lazy Resolution for Services

Don't pay for services you don't use.

"Your service provider registers 20 services.
But your request only needs 3 of them.
Why instantiate the other 17?
Lazy resolution defers instantiation until the service is actually used.
Your request becomes faster. Your memory usage drops."
⚠️ THE EAGER LOADING TRAP

Most Laravel service providers instantiate services immediately during registration (eager loading). This means every request pays the cost of every service, even if the request never uses them. For heavy services (e.g., PDF generators, image processors), this is wasted CPU and memory.

🔍 Eager Resolution vs Lazy Resolution

EAGER RESOLUTION (DEFAULT - WASTEFUL) ═══════════════════════════════════════════════════════════════════ ServiceProvider::register() │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ $this->app->bind(HeavyService::class, function () { │ │ return new HeavyService(); // ← INSTANTIATED NOW │ │ }); │ │ │ │ $this->app->bind(PdfGenerator::class, function () { │ │ return new PdfGenerator(); // ← INSTANTIATED NOW │ │ }); │ │ │ │ $this->app->bind(ImageProcessor::class, function () { │ │ return new ImageProcessor(); // ← INSTANTIATED NOW │ │ }); │ └─────────────────────────────────────────────────────────────────┘ │ ▼ Even if request doesn't use PdfGenerator → STILL INSTANTIATED! LAZY RESOLUTION (SMART - ON DEMAND) ═══════════════════════════════════════════════════════════════════ ServiceProvider::register() │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ $this->app->bind(HeavyService::class, HeavyService::class); │ │ // ↑ Laravel stores the class name, NOT an instance │ │ │ │ $this->app->bind(PdfGenerator::class, PdfGenerator::class); │ │ │ │ $this->app->bind(ImageProcessor::class, ImageProcessor::class);│ └─────────────────────────────────────────────────────────────────┘ │ ▼ Request that doesn't use PdfGenerator → NO INSTANTIATION! Request that uses PdfGenerator → INSTANTIATED ONLY THEN! Result: 70-90% reduction in service instantiation overhead.

📊 Performance Impact: Eager vs Lazy Resolution

Scenario Eager Resolution Lazy Resolution Improvement
Request using 3 of 20 services 20 services instantiated 3 services instantiated 85% fewer instantiations
Heavy service cost (PDF, Image, etc.) Always paid Only when used 100% reduction when not used
Memory per request (20 services) ~2-5 MB ~0.5-1 MB 70-80% reduction
CPU per request (service creation) 10-20ms 2-5ms 70-80% reduction
REAL-WORLD EXAMPLE

A typical Laravel app with 50 registered services:

⚙️ Implementing Lazy Resolution in Laravel

METHOD 1: Class Name String (Simplest)
// app/Providers/AppServiceProvider.php

public function register(): void
{
    // EAGER (BAD) - Instantiates immediately
    $this->app->bind(HeavyService::class, function ($app) {
        return new HeavyService(
            $app->make(Database::class),
            $app->make(Logger::class)
        );
    });
    
    // LAZY (GOOD) - No instantiation yet
    $this->app->bind(HeavyService::class, HeavyService::class);
    
    // For services with dependencies, use a closure that returns class name
    $this->app->bind(AnotherService::class, function ($app) {
        return AnotherService::class;  // ← Returns class name, not instance!
    });
}
METHOD 2: Lazy Binding for Interfaces
// Bind interface to implementation
$this->app->bind(PaymentGatewayInterface::class, StripeGateway::class);
// StripeGateway is NOT instantiated until requested

$this->app->bind(NotificationInterface::class, SmsNotification::class);
// SmsNotification is NOT instantiated until requested

// When you resolve the interface:
$payment = app(PaymentGatewayInterface::class);
// NOW StripeGateway is instantiated (lazy)
METHOD 3: Singleton with Lazy Resolution
// Singleton with lazy resolution
$this->app->singleton(ExpensiveService::class, function ($app) {
    // This closure only runs when the service is FIRST accessed
    return new ExpensiveService(
        $app->make(Database::class),
        $app->make(Cache::class)
    );
});

// Even singletons can be lazy! The service is only created once,
// but that creation happens on FIRST use, not on every request.

🎨 Lazy vs Eager Resolution (Visualized)

👎 EAGER RESOLUTION

ServiceProvider::register() │ ├── new HeavyService() ← Instantiated ├── new PdfGenerator() ← Instantiated ├── new ImageProcessor() ← Instantiated ├── new Mailer() ← Instantiated ├── new Logger() ← Instantiated └── ... 15 more services ← All instantiated Request arrives (only needs HeavyService) │ └── Uses HeavyService (already created) Waste: 19 services created for no reason!

👍 LAZY RESOLUTION

ServiceProvider::register() │ ├── bind HeavyService (store class name) ├── bind PdfGenerator (store class name) ├── bind ImageProcessor (store class name) └── ... 15 more services (all stored as names) Request arrives (needs HeavyService) │ ├── new HeavyService() ← Instantiated NOW └── Uses HeavyService Other services: NEVER instantiated. Perfect efficiency!

🔗 Lazy Resolution with Complex Dependencies

SERVICE WITH MULTIPLE DEPENDENCIES
class ReportGenerator
{
    public function __construct(
        private Database $db,
        private Cache $cache,
        private Logger $logger,
        private Mailer $mailer
    ) {}
}

// EAGER (BAD)
$this->app->bind(ReportGenerator::class, function ($app) {
    return new ReportGenerator(
        $app->make(Database::class),    // Instantiated now
        $app->make(Cache::class),       // Instantiated now
        $app->make(Logger::class),      // Instantiated now
        $app->make(Mailer::class)       // Instantiated now
    );
});  // All 5 services created even if ReportGenerator not used!

// LAZY (GOOD)
$this->app->bind(ReportGenerator::class, function ($app) {
    // Return a closure that will be resolved later
    return function () use ($app) {
        return new ReportGenerator(
            $app->make(Database::class),
            $app->make(Cache::class),
            $app->make(Logger::class),
            $app->make(Mailer::class)
        );
    };
});

// Usage
$generator = app(ReportGenerator::class);  // Returns closure
$instance = $generator();  // Now instantiated!
SIMPLER APPROACH

Laravel's container automatically handles lazy resolution for class names. Just use:

$this->app->bind(ReportGenerator::class, ReportGenerator::class);

Laravel will resolve dependencies lazily when the class is first instantiated.

🌍 Real-World Example: Admin-Only Services

THE SCENARIO

Your app has heavy services only used by admin users (e.g., PDF export, report generation, user import). But these services are instantiated on EVERY request, including public visitors.

THE SOLUTION
// app/Providers/AppServiceProvider.php

public function register(): void
{
    // These services are ONLY needed by admin
    // Using lazy resolution = only instantiated on admin requests
    $this->app->bind(PdfExportService::class, PdfExportService::class);
    $this->app->bind(ReportGenerator::class, ReportGenerator::class);
    $this->app->bind(UserImportService::class, UserImportService::class);
    $this->app->bind(BulkEmailService::class, BulkEmailService::class);
}

// Admin controller (only admin routes use these)
class AdminReportController
{
    public function export(ReportGenerator $generator)
    {
        // ReportGenerator is instantiated NOW (only on admin requests)
        // Public visitors never pay this cost
        return $generator->generate();
    }
}

Result: Public visitors (90% of traffic) don't pay the cost of admin-only services. 90% reduction in service instantiation overhead.

📦 Lazy Collections (Related Concept)

LAZY COLLECTIONS IN LARAVEL

Laravel also supports lazy collections for database queries:

// EAGER (loads all at once - heavy memory)
$users = User::all();  // 100k users → 500MB memory

// LAZY (loads one at a time - memory efficient)
$users = User::cursor();  // 100k users → 5MB memory

foreach ($users as $user) {
    // Process one user at a time
}

// Lazy Collection with chunking
$users = User::lazy();  // Similar to cursor

❌ When NOT to Use Lazy Resolution

AVOID LAZY RESOLUTION WHEN:
📌 THE RULE: Use lazy resolution for services that are NOT used on every request. Use eager resolution for services that are ALWAYS used. The goal is to avoid paying for what you don't use.

📝 Topic 23 Summary: Lazy Resolution for Services

Aspect Eager Resolution Lazy Resolution
Instantiation timing During service registration On first use
Memory per request All services Only used services
CPU per request All service constructors Only used service constructors
Best for Core services (used everywhere) Optional/additional services
📌 THE RULE: Register services with class name strings to enable lazy resolution. Only services that are guaranteed to be used on every request should be eagerly resolved.
NEXT TOPIC PREVIEW

Topic 24: Observers & withoutEvents — The silent performance killers. Why every model event triggers observers. How to disable events for bulk operations. When to move logic to queues.