📁 Volume II: Laravel Performance Tuning Kit

🪞 Topic 7: Service Container & Reflection Cost

Why auto-wiring is expensive and how to fix it.

"Laravel's service container is magical. But magic has a cost.
Every time you let the container 'figure things out', it uses Reflection — reading your code at runtime.
Reflection is CPU-intensive. And it happens on every single request."
THE HIDDEN COST

Laravel's auto-wiring is beautiful for development. You just type-hint a dependency, and Laravel figures out what to give you. But behind the scenes, it uses PHP's Reflection API to read your constructor signatures — on every request.

Reflection is 10-100x slower than direct instantiation. In a typical Laravel request, the container may resolve 20-50 dependencies. That's hundreds of Reflection operations per request.

🔍 What Is Reflection?

│ │ REFLECTION = CODE READING CODE AT RUNTIME │ ═══════════════════════════════════════════════════════════════════ │ │ When you write: │ ┌─────────────────────────────────────────────────────────────────┐ │ │ class UserController │ │ │ { │ │ │ public function __construct( │ │ │ UserService $service, │ │ │ Logger $logger, │ │ │ Cache $cache │ │ │ ) {} │ │ │ } │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ ▼ │ Laravel's container says: "I need to figure out what to pass." │ │ │ ▼ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ $reflection = new ReflectionClass(UserController::class); │ │ │ $constructor = $reflection->getConstructor(); │ │ │ $parameters = $constructor->getParameters(); │ │ │ │ │ │ foreach ($parameters as $param) { │ │ │ $type = $param->getType(); │ │ │ $className = $type->getName(); // "UserService" │ │ │ // Then recursively resolve UserService... │ │ │ } │ │ └─────────────────────────────────────────────────────────────────┘ │ │ THIS HAPPENS ON EVERY REQUEST! │
WHY IS REFLECTION SLOW?

Cost per Reflection operation: ~0.1-1ms. With 50 operations per request = 5-50ms of pure overhead.

📊 The Real Cost of Auto-Wiring

Benchmark: Resolving a simple service with 3 dependencies

👎 AUTO-WIRING (Reflection)

// Controller
class OrderController
{
    public function __construct(
        OrderService $orderService,
        PaymentService $paymentService,
        Logger $logger,
        Cache $cache,
        Mailer $mailer
    ) {}
}

// Resolved via container
$controller = app()->make(OrderController::class);

Cost per request: ~2-5ms (Reflection on 5 dependencies)

👍 MANUAL INSTANTIATION

// Controller
class OrderController
{
    public function __construct(
        private OrderService $orderService,
        private PaymentService $paymentService,
        private Logger $logger,
        private Cache $cache,
        private Mailer $mailer
    ) {}
}

// Create manually
$controller = new OrderController(
    new OrderService(),
    new PaymentService(),
    new Logger(),
    app('cache'),
    new Mailer()
);

Cost per request: ~0.05ms (no Reflection)

Real-world impact on a typical Laravel app (50 controllers, 20 services each):

ApproachReflection ops per requestTime per request10k requests/day
Full Auto-Wiring (default) ~200-500 20-50ms 200-500 seconds CPU
Singletons + Explicit Binding ~20-50 2-5ms 20-50 seconds CPU
Manual Instantiation (ideal) 0 0ms overhead 0 seconds overhead
BUT MANUAL INSTANTIATION IS PAINFUL

You don't need to go full manual. There's a middle ground: Singletons + Explicit Binding.

✅ The Fix: Singletons + Explicit Binding

THE GOLDEN RULE OF SERVICE CONTAINER

If a service has no request-specific state, make it a singleton. It will be resolved once and reused for all requests.

Step 1: Register Singletons in AppServiceProvider

// app/Providers/AppServiceProvider.php
public function register(): void
{
    // BAD: Transient (new instance every time)
    $this->app->bind(UserService::class, function ($app) {
        return new UserService($app->make(Logger::class));
    });
    
    // GOOD: Singleton (one instance, reused)
    $this->app->singleton(UserService::class, function ($app) {
        return new UserService($app->make(Logger::class));
    });
    
    // BEST: Explicit binding with type hints
    $this->app->when(UserController::class)
        ->needs(UserService::class)
        ->give(function ($app) {
            return $app->make(UserService::class);
        });
}

Step 2: Understand Singleton vs Transient

AspectTransient (bind)Singleton
Instantiated per request? Yes — every time No — once for all requests
Reflection cost per request? Yes — on every resolution Only on first resolution
Memory per request? N instances 1 instance
When to use Services with request-specific state (e.g., CartService) Stateless services (e.g., PaymentGateway, Mailer, Logger)
⚠️ SINGLETON WARNING

Only use singletons for stateless services. If your service stores request-specific data (like a shopping cart), making it a singleton will cause data leakage between users!

🎨 Transient vs Singleton (Visualized)

│ │ TRANSIENT (bind) — New instance every request │ ═══════════════════════════════════════════════════════════════════ │ │ Request 1: ┌─────────────────────────────────────────────────┐ │ │ │ new UserService() ← Reflection + instantiation │ │ │ │ new Logger() ← Reflection + instantiation │ │ │ │ new Cache() ← Reflection + instantiation │ │ │ └─────────────────────────────────────────────────┘ │ Request 2: ┌─────────────────────────────────────────────────┐ │ │ │ new UserService() ← Reflection + instantiation │ │ │ │ new Logger() ← Reflection + instantiation │ │ │ │ new Cache() ← Reflection + instantiation │ │ │ └─────────────────────────────────────────────────┘ │ Request 3: ┌─────────────────────────────────────────────────┐ │ │ │ new UserService() ← Reflection + instantiation │ │ │ │ new Logger() ← Reflection + instantiation │ │ │ │ new Cache() ← Reflection + instantiation │ │ │ └─────────────────────────────────────────────────┘ │ │ TOTAL: 3 instances. 9 Reflection operations. │ │ ═══════════════════════════════════════════════════════════════════ │ │ SINGLETON — One instance for all requests │ ═══════════════════════════════════════════════════════════════════ │ │ First request: ┌─────────────────────────────────────────────────┐ │ │ │ new UserService() ← Reflection + instantiation │ │ │ │ new Logger() ← Reflection + instantiation │ │ │ │ new Cache() ← Reflection + instantiation │ │ │ └─────────────────────────────────────────────────┘ │ Request 2: ┌─────────────────────────────────────────────────┐ │ │ │ reuse UserService() ← No Reflection │ │ │ │ reuse Logger() ← No Reflection │ │ │ │ reuse Cache() ← No Reflection │ │ │ └─────────────────────────────────────────────────┘ │ Request 3: ┌─────────────────────────────────────────────────┐ │ │ │ reuse UserService() ← No Reflection │ │ │ │ reuse Logger() ← No Reflection │ │ │ │ reuse Cache() ← No Reflection │ │ │ └─────────────────────────────────────────────────┘ │ │ TOTAL: 1 instance. 3 Reflection operations total. │

🔗 Explicit Binding: Tell Laravel What to Do

AUTO-WIRING IS SLOW

Every time Laravel sees a type-hint it hasn't seen before, it uses Reflection to figure out what to inject.

EXPLICIT BINDING = NO REFLECTION

When you explicitly tell Laravel what to inject, it doesn't need to reflect.

// app/Providers/AppServiceProvider.php
public function register(): void
{
    // Instead of letting Laravel figure this out via Reflection:
    // public function __construct(UserService $service, Logger $logger)
    
    // Tell Laravel explicitly:
    $this->app->when(UserController::class)
        ->needs(UserService::class)
        ->give(function ($app) {
            return $app->make(UserService::class);
        });
    
    $this->app->when(UserController::class)
        ->needs(Logger::class)
        ->give(function ($app) {
            return $app->make(Logger::class);
        });
    
    // For interfaces, always use explicit binding
    $this->app->bind(PaymentGatewayInterface::class, StripeGateway::class);
    $this->app->bind(NotificationInterface::class, SmsNotification::class);
}
BENEFIT

Laravel skips Reflection entirely for explicitly bound dependencies. It goes directly to the closure you provided.

⛔ Avoid make() Inside Loops

THE WORST PATTERN
foreach ($users as $user) {
    $service = app()->make(ProcessorService::class);  // 💀 Reflection N times!
    $service->process($user);
}

If you have 10,000 users, this runs Reflection 10,000 times. Your server will cry.

THE FIX: MOVE make() OUTSIDE
$service = app()->make(ProcessorService::class);  // ✅ Once
foreach ($users as $user) {
    $service->process($user);
}

👎 BEFORE (10,000 iterations)

foreach ($users as $user) {
    $service = app()->make(Service::class);
    // Reflection 10,000 times
}
// Time: 5-10 seconds

👍 AFTER (1 iteration)

$service = app()->make(Service::class);
foreach ($users as $user) {
    $service->process($user);
}
// Time: 0.5-1 second

⚙️ Compiled vs Runtime: Octane Changes Everything

WITH LARAVEL OCTANE

The service container is resolved ONCE when the application boots, not per request. Reflection cost drops to near zero.

│ │ WITHOUT OCTANE (Traditional PHP-FPM) │ ═══════════════════════════════════════════════════════════════════ │ │ Request 1: Boot app → Resolve all services → Handle → Die │ Request 2: Boot app → Resolve all services → Handle → Die │ Request 3: Boot app → Resolve all services → Handle → Die │ │ Reflection cost: EACH REQUEST │ │ ═══════════════════════════════════════════════════════════════════ │ │ WITH OCTANE (Swoole/RoadRunner) │ ═══════════════════════════════════════════════════════════════════ │ │ Application Start: Boot once → Resolve all services → Stay alive │ Request 1: Use cached services → Handle │ Request 2: Use cached services → Handle │ Request 3: Use cached services → Handle │ │ Reflection cost: ONCE EVER │
OCTANE WARNING

With Octane, memory leaks become permanent. If you have a singleton that accumulates data, it will keep growing until the server restarts. Always ensure your singletons are truly stateless.

📝 Topic 7 Summary: Service Container & Reflection

PatternReflection per RequestSpeedWhen to Use
Full Auto-Wiring (default) Many (200-500) Slow Development only
Transient (bind) Per resolution Slow Request-specific state
Singleton Once total Fast Stateless services
Explicit Binding None (direct) Fastest Critical paths, interfaces
Octane (persistent app) Once ever Lightning Production with Swoole/RoadRunner
📌 THE RULE: Make stateless services singletons. Bind interfaces explicitly. Never call make() inside loops. Use Octane for persistent applications.

Reflection is expensive. Every time you avoid it, your CPU thanks you.
NEXT TOPIC PREVIEW

Topic 8: Middleware Selectivity — Why every request runs 10 middleware classes. How to create specialized middleware groups and save 5-10ms per request.