Why Architecture Matters at Scale
Laravel is among the fastest frameworks to start with, but that ease of entry can encourage patterns that become painful at scale. The classic 'fat controller' anti-pattern — business logic piled directly into controller methods — collapses under weight: it is hard to test, hard to reuse, and hard to hand off to another developer. Enterprise applications need a deliberate layered architecture from day one.
The Service Layer Pattern
Extract all business logic into dedicated Service classes. Controllers become thin orchestrators that receive a request, delegate to a service, and return a response. This makes each layer independently testable and keeps the code organised as the application grows.
<?php
// app/Services/OrderService.php
namespace App\Services;
use App\Models\Order;
use App\Jobs\SendOrderConfirmation;
use App\Exceptions\InsufficientStockException;
use Illuminate\Support\Facades\DB;
class OrderService
{
public function __construct(
private readonly InventoryService $inventory,
private readonly PaymentService $payment,
) {}
public function placeOrder(array $data): Order
{
return DB::transaction(function () use ($data) {
$this->inventory->reserve($data['items']);
$order = Order::create($data);
$this->payment->charge($order);
SendOrderConfirmation::dispatch($order)->afterCommit();
return $order;
});
}
}Repository Pattern for Data Access
Wrap Eloquent behind repository interfaces. When you need to swap the data source (e.g. from MySQL to an external API) or write a unit test that should not touch the database, you simply swap the implementation — the rest of the application is oblivious to the change.
<?php
// app/Repositories/Contracts/OrderRepositoryInterface.php
namespace App\Repositories\Contracts;
use App\Models\Order;
use Illuminate\Pagination\LengthAwarePaginator;
interface OrderRepositoryInterface
{
public function findById(int $id): ?Order;
public function paginate(int $perPage = 15): LengthAwarePaginator;
public function create(array $data): Order;
}
// app/Repositories/EloquentOrderRepository.php
namespace App\Repositories;
use App\Models\Order;
use App\Repositories\Contracts\OrderRepositoryInterface;
class EloquentOrderRepository implements OrderRepositoryInterface
{
public function findById(int $id): ?Order
{
return Order::with(['items', 'customer'])->find($id);
}
public function paginate(int $perPage = 15): LengthAwarePaginator
{
return Order::latest()->paginate($perPage);
}
public function create(array $data): Order
{
return Order::create($data);
}
}Queues and Background Jobs
Never make the user wait for slow operations. Email sending, PDF generation, third-party API calls, and data imports all belong in background jobs dispatched to a queue. Laravel's queue system works with Redis, SQS, and database drivers out of the box.
<?php
// app/Jobs/GenerateInvoicePdf.php
namespace App\Jobs;
use App\Models\Order;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class GenerateInvoicePdf implements ShouldQueue
{
use Queueable;
public int $tries = 3;
public int $backoff = 60; // seconds between retries
public function __construct(private readonly Order $order) {}
public function handle(): void
{
$pdf = PDF::loadView('invoices.template', ['order' => $this->order]);
Storage::disk('s3')->put("invoices/{$this->order->id}.pdf", $pdf->output());
$this->order->update(['invoice_generated_at' => now()]);
}
}
// Dispatching from a controller or service
GenerateInvoicePdf::dispatch($order)->onQueue('documents');API Security Essentials
Security is non-negotiable in enterprise applications. These are the baseline controls every Laravel API should implement:
- Use Laravel Sanctum for SPA authentication and Passport for full OAuth 2.0 flows.
- Rate-limit all public endpoints with `throttle` middleware to prevent brute-force and DDoS.
- Validate every incoming request using Form Request classes — never trust raw input.
- Store secrets in `.env` and never commit them; use Laravel Vault or AWS Secrets Manager in production.
- Enable HTTPS everywhere and set `SESSION_SECURE_COOKIE=true` in production.
- Audit your routes with `php artisan route:list` — remove anything you don't need.
Testing: Feature and Unit Tests
A codebase without tests is a liability. Laravel's testing toolkit — built on PHPUnit, with first-class HTTP, database, and queue testing helpers — makes writing tests pleasant. Aim for high coverage on service and repository layers, and write feature tests that exercise entire HTTP flows.
<?php
// tests/Feature/OrderTest.php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class OrderTest extends TestCase
{
use RefreshDatabase;
public function test_authenticated_user_can_place_order(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)
->postJson('/api/orders', [
'items' => [
['product_id' => 1, 'quantity' => 2],
],
'payment_method' => 'card',
]);
$response->assertCreated()
->assertJsonStructure(['data' => ['id', 'status', 'total']]);
$this->assertDatabaseHas('orders', ['user_id' => $user->id]);
}
}Performance: Caching and Query Optimisation
Enterprise apps serve many concurrent users. Two levers have the highest ROI: caching and N+1 query elimination. Use `eager loading` (with() / load()) to prevent N+1 database hits, and cache expensive queries or aggregations in Redis. Laravel's Cache facade makes this trivial.
<?php
// Cache an expensive aggregation for 10 minutes
$stats = Cache::remember('dashboard:stats', 600, function () {
return [
'total_orders' => Order::count(),
'revenue_today' => Order::whereDate('created_at', today())->sum('total'),
'active_users' => User::active()->count(),
];
});
// Eliminate N+1: eager-load relationships
$orders = Order::with(['customer', 'items.product'])->latest()->get();