Scaling Modern Laravel: Architecture, Database Performance, and High-Throughput Systems
"High-traffic PHP applications demand architectural discipline, memory awareness, and predictable database access patterns. Default framework conventions priori"
High-traffic PHP applications demand architectural discipline, memory awareness, and predictable database access patterns. Default framework conventions prioritize rapid prototyping over high-throughput execution. Scaling Laravel requires decoupling domain workflows from the HTTP transport layer, eliminating query bottlenecks, hardening asynchronous workers against failure, and adopting persistent memory runtimes.
1. Clean Domain Boundaries: Pragmatic ADR and Action Pipelines
Laravel controllers often degrade into multi-responsibility classes handling validation, database transactions, third-party integrations, and response formatting. This coupling impedes testability and prevents logic reuse across CLI commands, queues, and API endpoints.
Adopt an Action-Domain-Responder (ADR) structure using strictly typed Data Transfer Objects (DTOs) and single-purpose domain actions.
// app/DTOs/RegisterOrderDTO.php
declare(strict_types=1);
namespace App\DTOs;
readonly class RegisterOrderDTO
{
/**
* @param array<int, array{name: string, price: int, quantity: int}> $lineItems
*/
public function __construct(
public int $userId,
public int $amountInCents,
public string $currency,
public array $lineItems,
) {}
/**
* @param array<string, mixed> $validated
*/
public static function fromRequest(array $validated): self
{
return new self(
userId: (int) $validated['user_id'],
amountInCents: (int) $validated['amount'],
currency: (string) $validated['currency'],
lineItems: (array) $validated['items'],
);
}
}
Controllers validate HTTP requests, map input to an immutable DTO, and delegate execution to the domain action:
// app/Http/Controllers/Order/StoreOrderController.php
declare(strict_types=1);
namespace App\Http\Controllers\Order;
use App\Actions\Order\CreateOrderAction;
use App\DTOs\RegisterOrderDTO;
use App\Http\Requests\Order\StoreOrderRequest;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
final class StoreOrderController
{
public function __invoke(StoreOrderRequest $request, CreateOrderAction $action): JsonResponse
{
$dto = RegisterOrderDTO::fromRequest($request->validated());
$order = $action->execute($dto);
return new JsonResponse($order, Response::HTTP_CREATED);
}
}
Domain actions execute transactional state changes and dispatch domain events:
// app/Actions/Order/CreateOrderAction.php
declare(strict_types=1);
namespace App\Actions\Order;
use App\DTOs\RegisterOrderDTO;
use App\Events\OrderCreatedEvent;
use App\Models\Order;
use Illuminate\Support\Facades\DB;
final class CreateOrderAction
{
public function execute(RegisterOrderDTO $dto): Order
{
return DB::transaction(function () use ($dto): Order {
$order = Order::query()->create([
'user_id' => $dto->userId,
'amount_cents' => $dto->amountInCents,
'currency' => $dto->currency,
'status' => 'pending',
]);
$order->items()->createMany($dto->lineItems);
event(new OrderCreatedEvent($order->id));
return $order;
});
}
}
This pattern isolates domain logic from HTTP concerns, allowing execution across workers, CLI commands, and HTTP endpoints without duplication.
2. Database Optimization: Query Discipline and Keyset Pagination
Active Record abstractions obscure underlying query volume and execution cost. Latency spikes in production systems typically stem from unindexed lookups, unbounded result sets, and accidental queries.
Enforcing Strict Query Bounds
Prevent accidental lazy loading and unfillable attributes across non-production environments in AppServiceProvider:
// app/Providers/AppServiceProvider.php
namespace App\Providers;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Model::shouldBeStrict(! $this->app->isProduction());
}
}
This configuration throws runtime exceptions during automated testing and local execution when lazy loading occurs or unguarded attributes are accessed.
Offset vs. Keyset (Cursor) Pagination
Traditional offset pagination degrades linearly as table depth increases. The database engine must scan and discard all rows preceding the designated offset:
where is the offset depth and is the page size limit. Scanning past requires reading one million rows before returning the targeted records.
Keyset pagination eliminates offset scanning by filtering on an indexed, sequential column (such as an auto-incrementing ID or composite timestamp index):
where represents total indexed rows.
// Keyset query utilizing sequential B-Tree index traversal
Order::query()
->where('id', '<', $cursorId)
->orderByDesc('id')
->limit(25)
->get();
Laravel provides native cursor pagination for indexed sequential columns:
$orders = Order::query()
->where('status', 'completed')
->orderByDesc('id')
->cursorPaginate(25);
3. Resilient Queue Pipelines: Idempotency and Backoff Topology
Distributed background processing requires workers capable of handling unpredictable network latency, third-party outages, and worker terminations without data corruption. Every queued job must be idempotent.
Exponential Backoff with Jitter
Fixed retry intervals risk overwhelming recovering downstream services (thundering herd problem). Combine exponential backoff with randomized jitter:
Enforce backoff sequences and distributed lock acquisition directly within the job:
// app/Jobs/ProcessPaymentJob.php
declare(strict_types=1);
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Cache;
class ProcessPaymentJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 5;
public int $timeout = 60;
public array $backoff = [2, 10, 30, 120, 300];
public function __construct(
public string $paymentUuid,
public int $amountInCents,
) {}
public function handle(): void
{
$lock = Cache::lock("lock:payment:{$this->paymentUuid}", 30);
if (! $lock->get()) {
$this->release(5);
return;
}
try {
// Process payment transaction idempotently against external gateway
} finally {
$lock->release();
}
}
}
Queue Partitioning via Laravel Horizon
Partition queue workers by operational Service Level Agreements (SLAs) in config/horizon.php:
'environments' => [
'production' => [
'supervisor-critical' => [
'connection' => 'redis',
'queue' => ['payments', 'orders'],
'balance' => 'auto',
'minProcesses' => 5,
'maxProcesses' => 25,
'tries' => 3,
],
'supervisor-default' => [
'connection' => 'redis',
'queue' => ['notifications', 'exports', 'webhooks'],
'balance' => 'simple',
'minProcesses' => 2,
'maxProcesses' => 10,
'tries' => 2,
],
],
],
Isolating high-priority transaction queues prevents low-priority batch workloads from starving critical business events.
4. Persistent Runtimes: Laravel Octane
Standard PHP-FPM executes under a shared-nothing lifecycle: each HTTP request boots the framework, parses configuration files, and initializes service providers.
Laravel Octane maintains the application instance in memory using RoadRunner or Swoole. Bypassing repeated framework bootstrapping reduces response latency from tens of milliseconds to sub-millisecond execution times.
Preventing State and Memory Leaks
Persistent runtimes retain static variables, container singletons, and static state across request boundaries. Retaining contextual data between requests introduces security vulnerabilities and memory leaks:
// UNPREDICTABLE IN OCTANE: Static state persists across requests
class RequestContext
{
public static ?User $currentUser = null;
}
Flush stateful bindings on each cycle using Octane termination listeners:
// app/Providers/AppServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Laravel\Octane\Events\RequestTerminated;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app['events']->listen(RequestTerminated::class, function () {
// Reset stateful singletons and custom registries
});
}
}
Run Octane workers with bounded request lifecycles to mitigate memory fragmentation:
php artisan octane:start --server=roadrunner --workers=8 --max-requests=10000
The --max-requests flag periodically recycles worker processes, clearing residual memory buildup without dropping active connections.
5. Pipeline Pattern for Complex Business Workflows
Multi-stage domain workflows—such as checkout validation, discount application, regional taxation, and fraud evaluation—accumulate structural bloat when structured as procedural scripts. The Laravel Pipeline provides composable, isolated execution chains.
// app/Pipelines/Order/ProcessOrderPipeline.php
declare(strict_types=1);
namespace App\Pipelines\Order;
use App\DTOs\OrderContext;
use Illuminate\Pipeline\Pipeline;
final class ProcessOrderPipeline
{
public function __construct(private Pipeline $pipeline) {}
public function process(OrderContext $context): OrderContext
{
$pipes = [
Pipes\ValidateInventory::class,
Pipes\ApplyDiscountVouchers::class,
Pipes\CalculateRegionalTax::class,
Pipes\EvaluateFraudRisk::class,
];
return $this->pipeline
->send($context)
->through($pipes)
->then(fn (OrderContext $context): OrderContext => $context);
}
}
Each pipe encapsulates a single transformation or invariant check:
// app/Pipelines/Order/Pipes/ValidateInventory.php
declare(strict_types=1);
namespace App\Pipelines\Order\Pipes;
use App\DTOs\OrderContext;
use Closure;
use DomainException;
final class ValidateInventory
{
public function handle(OrderContext $context, Closure $next): mixed
{
if (! $context->hasSufficientStock()) {
throw new DomainException('Insufficient stock for order line items.');
}
return $next($context);
}
}
6. Integration Testing Against Real Data Stores
Unit testing query builder logic using mocks validates syntax rather than database behavior. Test domain actions and queries against real database instances inside transactions using RefreshDatabase.
// tests/Feature/Order/CreateOrderActionTest.php
declare(strict_types=1);
namespace Tests\Feature\Order;
use App\Actions\Order\CreateOrderAction;
use App\DTOs\RegisterOrderDTO;
use App\Events\OrderCreatedEvent;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
final class CreateOrderActionTest extends TestCase
{
use RefreshDatabase;
public function test_creates_order_and_dispatches_event(): void
{
Event::fake([OrderCreatedEvent::class]);
$user = User::factory()->create();
$dto = new RegisterOrderDTO(
userId: $user->id,
amountInCents: 5000,
currency: 'USD',
lineItems: [['name' => 'Widget', 'price' => 5000, 'quantity' => 1]],
);
$action = $this->app->make(CreateOrderAction::class);
$order = $action->execute($dto);
$this->assertDatabaseHas('orders', [
'id' => $order->id,
'user_id' => $user->id,
'amount_cents' => 5000,
'status' => 'pending',
]);
Event::assertDispatched(OrderCreatedEvent::class);
}
}
Execute parallelized test runs across available hardware threads:
php artisan test --parallel --recreate-databases
Architectural Comparison Matrix
| Layer | Standard Laravel Pattern | Scaled High-Throughput Pattern | Primary Trade-Off |
|---|---|---|---|
| Domain Logic | Fat Controllers & Eloquent Observers | ADR, DTOs, and Dedicated Actions | Higher file count, clear boundaries |
| Data Access | Offset paginate() |
Keyset cursorPaginate() |
Eliminates arbitrary page jumping |
| Runtime | PHP-FPM Request-Bound | Laravel Octane (RoadRunner/Swoole) | Requires strict memory management |
| Queue Workers | Shared Single Queue Worker | Segmented Horizon Workers + Jitter | Requires dedicated Redis topology |
| Workflows | Monolithic Service Classes | Composable Pipeline Execution |
Additional class orchestration |
Scaling Laravel requires moving past generic Active Record patterns toward explicit domain actions, constant-time database access, isolated queue topologies, and memory-conscious execution runtimes. Applying these patterns preserves maintainability while sustaining enterprise traffic loads.
About the Author
huud
@huud
Systems architect and software engineer building high-performance distributed platforms.