• Laravel
  • Building Scalable Laravel Applications

    Building Scalable Laravel Applications

    When building modern web applications that need to serve thousands of users and handle complex business logic, scalability is a must. After building well-architected Laravel applications in production, I've gathered patterns and practices that transform simple Laravel apps into robust, maintainable systems. Today, I'll walk you through these battle-tested strategies that seamlessly scale from MVP to enterprise.

    #The Foundation: Action-Based Architecture

    The most transformative pattern I've encountered is the Action pattern—a simple concept that revolutionizes how you organize business logic. Instead of bloated controllers doing everything, actions encapsulate single business operations.

     1abstract class BaseAction
     2{
     3    public static function instance(): static
     4    {
     5        return new static();
     6    }
     7    
     8    abstract public function handle(/* parameters */);
     9}
    10
    11class CreateSubscriptionAction extends BaseAction
    12{
    13    public function handle(User $user, Plan $plan, array $paymentData): Subscription
    14    {
    15        DB::beginTransaction();
    16        try {
    17            $subscription = $this->createSubscription($user, $plan);
    18            $this->processPayment($subscription, $paymentData);
    19            $this->sendWelcomeNotification($user);
    20            $this->logSubscriptionEvent($subscription);
    21            
    22            DB::commit();
    23            return $subscription;
    24        } catch (Exception $e) {
    25            DB::rollBack();
    26            throw $e;
    27        }
    28    }
    29    
    30    private function createSubscription(User $user, Plan $plan): Subscription
    31    {
    32        return $user->subscriptions()->create([
    33            'plan_id' => $plan->id,
    34            'status' => 'active',
    35            'expires_at' => now()->addDays($plan->duration_days),
    36        ]);
    37    }
    38    
    39    private function processPayment(Subscription $subscription, array $paymentData): void
    40    {
    41        // Payment processing logic
    42    }
    43    
    44    private function sendWelcomeNotification(User $user): void
    45    {
    46        Mail::to($user)->queue(new SubscriptionWelcome($user));
    47    }
    48    
    49    private function logSubscriptionEvent(Subscription $subscription): void
    50    {
    51        activity()->log('Subscription created', $subscription);
    52    }
    53}
    

    Why does this work so brilliantly? Single responsibility. Each action has one job, making it easily testable, reusable across controllers and commands, and maintainable as your application grows.

    The genius here is that actions serve as the single source of truth for business operations across your entire application stack.

    #Query Optimization: The Pipeline Filter Pattern

    Dynamic query building is where most Laravel applications start to feel sluggish. The pipeline filter pattern solves this elegantly by transforming query building into composable, testable components:

     1abstract class Filter
     2{
     3    public function handle(Builder $builder, Closure $next)
     4    {
     5        if (!request()->has($this->filterName())) {
     6            return $next($builder);
     7        }
     8        return $this->applyFilter($next($builder));
     9    }
    10    
    11    abstract protected function applyFilter(Builder $builder): Builder;
    12    
    13    protected function filterName(): string
    14    {
    15        return Str::snake(class_basename($this));
    16    }
    17}
    18
    19class StatusFilter extends Filter
    20{
    21    protected function applyFilter(Builder $builder): Builder
    22    {
    23        $status = request($this->filterName());
    24        
    25        return match($status) {
    26            'active' => $builder->where('status', 'active'),
    27            'inactive' => $builder->where('status', 'inactive'),
    28            'pending' => $builder->where('status', 'pending'),
    29            default => $builder,
    30        };
    31    }
    32}
    33
    34class DateRangeFilter extends Filter
    35{
    36    protected function applyFilter(Builder $builder): Builder
    37    {
    38        return $builder
    39            ->when(request('start_date'), fn($q) => 
    40                $q->whereDate('created_at', '>=', request('start_date')))
    41            ->when(request('end_date'), fn($q) => 
    42                $q->whereDate('created_at', '<=', request('end_date')));
    43    }
    44}
    45
    46// Model trait
    47trait HasFiltered
    48{
    49    protected array $filter_pipes = [
    50        SearchFilter::class,
    51        StatusFilter::class,
    52        DateRangeFilter::class,
    53    ];
    54    
    55    public function scopeWhereFilterByQueryParams(Builder $query): Builder
    56    {
    57        return Pipeline::send($query)
    58            ->through($this->getFilterPipes())
    59            ->thenReturn();
    60    }
    61    
    62    protected function getFilterPipes(): array
    63    {
    64        return $this->filter_pipes ?? [];
    65    }
    66}
    67
    68// Clean controller usage
    69class SubscriptionController extends Controller
    70{
    71    public function index()
    72    {
    73        $subscriptions = Subscription::with(['user', 'plan'])
    74            ->whereFilterByQueryParams()
    75            ->paginate();
    76            
    77        return SubscriptionResource::collection($subscriptions);
    78    }
    79}
    

    This approach provides several benefits:

    • Composable filters: Each filter is independent and testable
    • Performance optimization: Only active filters are applied
    • Maintainable logic: Adding new filters requires no controller changes

    #Type Safety Through Modern PHP

    Laravel applications benefit tremendously from PHP 8.1+'s type system. The combination of enums, readonly properties, and DTOs creates compile-time safety that prevents entire categories of runtime errors.

     1enum SubscriptionStatus: string
     2{
     3    case ACTIVE = 'active';
     4    case CANCELLED = 'cancelled';
     5    case EXPIRED = 'expired';
     6    case PENDING = 'pending';
     7    
     8    public static function all(): array
     9    {
    10        return array_column(self::cases(), 'value');
    11    }
    12    
    13    public function getLabel(): string
    14    {
    15        return match($this) {
    16            self::ACTIVE => 'Active Subscription',
    17            self::CANCELLED => 'Cancelled',
    18            self::EXPIRED => 'Expired',
    19            self::PENDING => 'Pending Activation',
    20        };
    21    }
    22    
    23    public function canRenew(): bool
    24    {
    25        return in_array($this, [self::EXPIRED, self::CANCELLED]);
    26    }
    27}
    28
    29// DTO for type-safe data transfer
    30class SubscriptionDataDto extends BaseDto
    31{
    32    public function __construct(
    33        public readonly int $user_id,
    34        public readonly int $plan_id,
    35        public readonly SubscriptionStatus $status,
    36        public readonly ?Carbon $expires_at = null,
    37        public readonly array $metadata = [],
    38    ) {
    39        if ($this->status === SubscriptionStatus::ACTIVE && !$this->expires_at) {
    40            throw new InvalidArgumentException('Active subscriptions must have expiration date');
    41        }
    42    }
    43    
    44    public static function fromArray(array $data): static
    45    {
    46        return new self(
    47            user_id: (int) $data['user_id'],
    48            plan_id: (int) $data['plan_id'],
    49            status: SubscriptionStatus::from($data['status']),
    50            expires_at: isset($data['expires_at']) ? Carbon::parse($data['expires_at']) : null,
    51            metadata: $data['metadata'] ?? [],
    52        );
    53    }
    54    
    55    public function isExpiringSoon(int $days = 7): bool
    56    {
    57        return $this->expires_at && 
    58               $this->expires_at->diffInDays(now()) <= $days;
    59    }
    60}
    

    The power of this approach lies in prevention rather than detection. Type errors are caught at development time, not in production.

    #Event-Driven Decoupling

    One of the most elegant patterns for scaling Laravel applications is event-driven architecture. This approach decouples your core business logic from side effects, making your system more maintainable and testable.

     1// Events
     2class SubscriptionCreated
     3{
     4    use Dispatchable, SerializesModels;
     5    
     6    public function __construct(public readonly Subscription $subscription) {}
     7}
     8
     9class SubscriptionExpired
    10{
    11    use Dispatchable, SerializesModels;
    12    
    13    public function __construct(public readonly Subscription $subscription) {}
    14}
    15
    16// Listeners
    17class SendWelcomeEmailListener implements ShouldQueue
    18{
    19    use InteractsWithQueue;
    20    
    21    public function handle(SubscriptionCreated $event): void
    22    {
    23        $subscription = $event->subscription;
    24        
    25        Mail::to($subscription->user)
    26            ->queue(new SubscriptionWelcomeEmail($subscription));
    27    }
    28}
    29
    30class UpdateUserPermissionsListener
    31{
    32    public function handle(SubscriptionCreated $event): void
    33    {
    34        $user = $event->subscription->user;
    35        $plan = $event->subscription->plan;
    36        
    37        $user->syncPermissions($plan->permissions);
    38    }
    39}
    40
    41class LogSubscriptionMetricsListener
    42{
    43    public function handle(SubscriptionCreated $event): void
    44    {
    45        UpdateCompanyMetricsJob::dispatch('subscription_created');
    46    }
    47}
    48
    49// Event registration in AppServiceProvider
    50public function boot(): void
    51{
    52    $eventsToListeners = [
    53        SubscriptionCreated::class => [
    54            SendWelcomeEmailListener::class,
    55            UpdateUserPermissionsListener::class,
    56            LogSubscriptionMetricsListener::class,
    57        ],
    58        SubscriptionExpired::class => [
    59            RevokeUserPermissionsListener::class,
    60            SendRenewalEmailListener::class,
    61        ],
    62    ];
    63    
    64    foreach ($eventsToListeners as $event => $listeners) {
    65        foreach ($listeners as $listener) {
    66            Event::listen($event, $listener);
    67        }
    68    }
    69}
    70
    71// Clean action usage
    72class CreateSubscriptionAction extends BaseAction
    73{
    74    public function handle(SubscriptionDataDto $data): Subscription
    75    {
    76        $subscription = Subscription::create($data->toArray());
    77        
    78        // Single line - all side effects handled by listeners
    79        SubscriptionCreated::dispatch($subscription);
    80        
    81        return $subscription;
    82    }
    83}
    

    This pattern ensures that your core business logic remains focused while side effects are handled asynchronously and independently.

    #Authorization Architecture That Scales

    Complex applications require sophisticated authorization. Laravel's Gate system combined with helper functions creates a centralized, maintainable authorization layer:

     1// Service Provider
     2class AuthServiceProvider extends ServiceProvider
     3{
     4    public function boot(): void
     5    {
     6        Gate::define('manage-subscriptions', function (User $user): bool {
     7            return $user->hasPermission('manage_subscriptions') || $user->isAdmin();
     8        });
     9        
    10        Gate::define('view-subscription', function (User $user, Subscription $subscription): bool {
    11            return $user->id === $subscription->user_id || 
    12                   $user->hasPermission('view_all_subscriptions');
    13        });
    14        
    15        Gate::define('cancel-subscription', function (User $user, Subscription $subscription): bool {
    16            if ($user->id !== $subscription->user_id) {
    17                return false;
    18            }
    19            
    20            return $subscription->status === SubscriptionStatus::ACTIVE;
    21        });
    22    }
    23}
    24
    25// Global helper functions
    26function canManageSubscriptions(): bool
    27{
    28    return Gate::check('manage-subscriptions');
    29}
    30
    31function canViewSubscription(Subscription $subscription): bool
    32{
    33    return Gate::check('view-subscription', $subscription);
    34}
    35
    36function canCancelSubscription(Subscription $subscription): bool
    37{
    38    return Gate::check('cancel-subscription', $subscription);
    39}
    40
    41// Form request authorization
    42class UpdateSubscriptionRequest extends FormRequest
    43{
    44    public function authorize(): bool
    45    {
    46        return canManageSubscriptions() || 
    47               (auth()->id() === $this->route('subscription')->user_id);
    48    }
    49    
    50    public function rules(): array
    51    {
    52        return [
    53            'status' => ['required', Rule::in(SubscriptionStatus::all())],
    54            'expires_at' => ['nullable', 'date', 'after:today'],
    55        ];
    56    }
    57}
    58
    59// Middleware for route-level protection
    60class SubscriptionAccessMiddleware
    61{
    62    public function handle(Request $request, Closure $next): Response
    63    {
    64        $subscription = $request->route('subscription');
    65        
    66        if (!canViewSubscription($subscription)) {
    67            abort(403, 'Access denied to this subscription');
    68        }
    69        
    70        return $next($request);
    71    }
    72}
    

    This centralized approach prevents authorization logic from being scattered across controllers and makes security policies easy to audit and modify.

    #Performance-First Queue Management

    Background job processing is crucial for scalable applications. Implementing robust queue patterns ensures your application remains responsive under load:

     1class ProcessSubscriptionPaymentJob implements ShouldQueue
     2{
     3    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
     4    
     5    public $tries = 3;
     6    public $backoff = [60, 300, 900]; // Exponential backoff
     7    public $timeout = 120;
     8    
     9    public function __construct(
    10        private readonly Subscription $subscription,
    11        private readonly array $paymentData
    12    ) {}
    13    
    14    public function handle(): void
    15    {
    16        try {
    17            $payment = PaymentService::charge(
    18                $this->subscription->user,
    19                $this->subscription->plan->price,
    20                $this->paymentData
    21            );
    22            
    23            $this->subscription->update([
    24                'status' => SubscriptionStatus::ACTIVE,
    25                'last_payment_at' => now(),
    26            ]);
    27            
    28            Log::info('Subscription payment processed', [
    29                'subscription_id' => $this->subscription->id,
    30                'payment_id' => $payment->id,
    31            ]);
    32            
    33        } catch (PaymentFailedException $e) {
    34            Log::error('Subscription payment failed', [
    35                'subscription_id' => $this->subscription->id,
    36                'error' => $e->getMessage(),
    37                'attempt' => $this->attempts(),
    38            ]);
    39            
    40            throw $e; // Trigger retry
    41        }
    42    }
    43    
    44    public function failed(Throwable $exception): void
    45    {
    46        $this->subscription->update([
    47            'status' => SubscriptionStatus::CANCELLED,
    48            'cancelled_reason' => 'Payment failed after retries',
    49        ]);
    50        
    51        Mail::to($this->subscription->user)
    52            ->send(new PaymentFailedNotification($this->subscription));
    53    }
    54}
    55
    56// Batch processing for efficiency
    57class SendRenewalRemindersJob implements ShouldQueue
    58{
    59    public function handle(): void
    60    {
    61        $expiringSubscriptions = Subscription::where('status', SubscriptionStatus::ACTIVE)
    62            ->whereBetween('expires_at', [now()->addDays(7), now()->addDays(8)])
    63            ->with('user')
    64            ->get();
    65        
    66        $jobs = $expiringSubscriptions->map(function ($subscription) {
    67            return new SendRenewalReminderJob($subscription);
    68        });
    69        
    70        Bus::batch($jobs)
    71            ->name('Renewal Reminders - ' . now()->format('Y-m-d'))
    72            ->allowFailures()
    73            ->onQueue('emails')
    74            ->dispatch();
    75    }
    76}
    77
    78// Unique jobs to prevent duplicates
    79class UpdateSubscriptionMetricsJob implements ShouldQueue, ShouldBeUnique
    80{
    81    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    82    
    83    public $uniqueFor = 3600; // 1 hour
    84    
    85    public function uniqueId(): string
    86    {
    87        return 'subscription-metrics-update';
    88    }
    89    
    90    public function handle(): void
    91    {
    92        $metrics = [
    93            'total_active' => Subscription::where('status', SubscriptionStatus::ACTIVE)->count(),
    94            'total_revenue' => Subscription::sum('total_paid'),
    95            'churn_rate' => $this->calculateChurnRate(),
    96        ];
    97        
    98        cache()->put('subscription_metrics', $metrics, now()->addHour());
    99    }
    100    
    101    private function calculateChurnRate(): float
    102    {
    103        $totalCancelled = Subscription::where('status', SubscriptionStatus::CANCELLED)
    104            ->whereMonth('cancelled_at', now()->month)
    105            ->count();
    106            
    107        $totalActive = Subscription::where('status', SubscriptionStatus::ACTIVE)->count();
    108        
    109        return $totalActive > 0 ? ($totalCancelled / $totalActive) * 100 : 0;
    110    }
    111}
    

    This queue architecture provides automatic retries, failure handling, and prevents duplicate processing while maintaining high throughput.

    #API Design for Long-Term Maintenance

    Consistent API responses and resource transformation patterns make your API a joy to consume and maintain:

     1class SubscriptionResource extends JsonResource
     2{
     3    public function toArray(Request $request): array
     4    {
     5        return [
     6            'id' => $this->id,
     7            'status' => $this->status->value,
     8            'status_label' => $this->status->getLabel(),
     9            'plan' => new PlanResource($this->whenLoaded('plan')),
    10            'user' => new UserResource($this->whenLoaded('user')),
    11            'expires_at' => $this->expires_at?->toISOString(),
    12            'created_at' => $this->created_at->toISOString(),
    13            'updated_at' => $this->updated_at->toISOString(),
    14            
    15            // Computed properties
    16            'is_active' => $this->status === SubscriptionStatus::ACTIVE,
    17            'is_expiring_soon' => $this->expires_at && 
    18                                 $this->expires_at->diffInDays(now()) <= 7,
    19            'days_remaining' => $this->expires_at?->diffInDays(now()),
    20            
    21            // Conditional fields
    22            'cancellation_reason' => $this->when(
    23                $this->status === SubscriptionStatus::CANCELLED,
    24                $this->cancellation_reason
    25            ),
    26            
    27            // Actions available to current user
    28            'can_cancel' => $this->when(
    29                auth()->check(),
    30                fn() => canCancelSubscription($this->resource)
    31            ),
    32            'can_renew' => $this->when(
    33                auth()->check(),
    34                fn() => $this->status->canRenew()
    35            ),
    36        ];
    37    }
    38}
    39
    40// Consistent API response trait
    41trait ApiResponses
    42{
    43    protected function successResponse($data = null, string $message = 'Success', int $code = 200): JsonResponse
    44    {
    45        return response()->json([
    46            'success' => true,
    47            'message' => $message,
    48            'data' => $data,
    49            'timestamp' => now()->toISOString(),
    50        ], $code);
    51    }
    52    
    53    protected function errorResponse(string $message, int $code = 400, array $errors = []): JsonResponse
    54    {
    55        return response()->json([
    56            'success' => false,
    57            'message' => $message,
    58            'errors' => $errors,
    59            'timestamp' => now()->toISOString(),
    60        ], $code);
    61    }
    62    
    63    protected function paginatedResponse($data, string $message = 'Success'): JsonResponse
    64    {
    65        return response()->json([
    66            'success' => true,
    67            'message' => $message,
    68            'data' => $data->items(),
    69            'pagination' => [
    70                'current_page' => $data->currentPage(),
    71                'per_page' => $data->perPage(),
    72                'total' => $data->total(),
    73                'last_page' => $data->lastPage(),
    74                'has_more' => $data->hasMorePages(),
    75            ],
    76            'timestamp' => now()->toISOString(),
    77        ]);
    78    }
    79}
    80
    81// Clean controller implementation
    82class SubscriptionController extends Controller
    83{
    84    use ApiResponses;
    85    
    86    public function index(IndexSubscriptionRequest $request)
    87    {
    88        $subscriptions = Subscription::with(['user', 'plan'])
    89            ->whereFilterByQueryParams()
    90            ->paginate();
    91            
    92        return $this->paginatedResponse(
    93            SubscriptionResource::collection($subscriptions)
    94        );
    95    }
    96    
    97    public function store(StoreSubscriptionRequest $request, CreateSubscriptionAction $action)
    98    {
    99        try {
    100            $dto = SubscriptionDataDto::fromArray($request->validated());
    101            $subscription = $action->handle($dto);
    102            
    103            return $this->successResponse(
    104                new SubscriptionResource($subscription->load(['user', 'plan'])),
    105                'Subscription created successfully',
    106                201
    107            );
    108        } catch (PaymentFailedException $e) {
    109            return $this->errorResponse('Payment failed: ' . $e->getMessage(), 402);
    110        }
    111    }
    112}
    

    This resource pattern provides consistency, security, and flexibility while making API responses self-documenting.

    #Smart Caching Strategies

    Intelligent caching can transform application performance. Here's a modern approach that doesn't rely on deprecated cache tags:

     1trait HasModelCache
     2{
     3    public static function bootHasModelCache()
     4    {
     5        static::saved(function ($model) {
     6            $model->clearModelCache();
     7        });
     8        
     9        static::deleted(function ($model) {
    10            $model->clearModelCache();
    11        });
    12    }
    13    
    14    public function cacheKey(string $suffix = ''): string
    15    {
    16        return sprintf('%s:%s%s', 
    17            class_basename($this), 
    18            $this->getKey(),
    19            $suffix ? ":{$suffix}" : ''
    20        );
    21    }
    22    
    23    public function rememberForever(string $key, callable $callback)
    24    {
    25        return cache()->rememberForever($this->cacheKey($key), $callback);
    26    }
    27    
    28    public function remember(string $key, int $ttl, callable $callback)
    29    {
    30        return cache()->remember($this->cacheKey($key), $ttl, $callback);
    31    }
    32    
    33    protected function getRelatedCacheKeys(): array
    34    {
    35        return [
    36            class_basename($this) . ':*',
    37            $this->cacheKey() . ':*',
    38        ];
    39    }
    40    
    41    public function clearModelCache(): void
    42    {
    43        // Clear specific model caches
    44        $keys = $this->getRelatedCacheKeys();
    45        
    46        foreach ($keys as $pattern) {
    47            $this->clearCacheByPattern($pattern);
    48        }
    49        
    50        // Clear class-level caches
    51        $this->clearClassCache();
    52    }
    53    
    54    private function clearCacheByPattern(string $pattern): void
    55    {
    56        if (method_exists(cache()->getStore(), 'flush')) {
    57            // For cache stores that support pattern-based clearing
    58            $cacheKeys = cache()->getStore()->getAllKeys();
    59            $keysToDelete = array_filter($cacheKeys, function ($key) use ($pattern) {
    60                return fnmatch($pattern, $key);
    61            });
    62            
    63            foreach ($keysToDelete as $key) {
    64                cache()->forget($key);
    65            }
    66        }
    67    }
    68    
    69    private function clearClassCache(): void
    70    {
    71        // Clear common class-level caches
    72        cache()->forget(class_basename($this) . ':count');
    73        cache()->forget(class_basename($this) . ':metrics');
    74    }
    75}
    76
    77// Usage in models
    78class Subscription extends Model
    79{
    80    use HasModelCache;
    81    
    82    public function getActiveSubscriptionsCount(): int
    83    {
    84        return $this->rememberForever('active_count', function () {
    85            return self::where('status', SubscriptionStatus::ACTIVE)->count();
    86        });
    87    }
    88    
    89    public function getRevenueMetrics(): array
    90    {
    91        return cache()->remember('subscription:revenue_metrics', 3600, function () {
    92            return [
    93                'total_revenue' => self::sum('total_paid'),
    94                'monthly_revenue' => self::whereMonth('created_at', now()->month)->sum('total_paid'),
    95                'average_subscription_value' => self::avg('total_paid'),
    96            ];
    97        });
    98    }
    99}
    100
    101// Centralized cache management service
    102class CacheService
    103{
    104    public static function clearSubscriptionCaches(): void
    105    {
    106        $patterns = [
    107            'subscription:*',
    108            'Subscription:*',
    109            'user_subscriptions:*',
    110            'popular_plans',
    111            'subscription_metrics',
    112        ];
    113        
    114        foreach ($patterns as $pattern) {
    115            self::forgetByPattern($pattern);
    116        }
    117    }
    118    
    119    public static function clearUserCaches(int $userId): void
    120    {
    121        $patterns = [
    122            "user:{$userId}:*",
    123            "User:{$userId}:*",
    124            "user_subscriptions:{$userId}",
    125        ];
    126        
    127        foreach ($patterns as $pattern) {
    128            self::forgetByPattern($pattern);
    129        }
    130    }
    131    
    132    private static function forgetByPattern(string $pattern): void
    133    {
    134        if (str_contains($pattern, '*')) {
    135            // Handle wildcard patterns
    136            if (config('cache.default') === 'redis') {
    137                // Redis supports pattern deletion
    138                $keys = Redis::keys($pattern);
    139                if (!empty($keys)) {
    140                    Redis::del($keys);
    141                }
    142            }
    143        } else {
    144            // Direct key deletion
    145            cache()->forget($pattern);
    146        }
    147    }
    148}
    149
    150// Query result caching with automatic invalidation
    151class SubscriptionService
    152{
    153    public function getPopularPlans(int $limit = 10): Collection
    154    {
    155        return cache()->remember('popular_plans', 1800, function () use ($limit) {
    156            return Plan::withCount('subscriptions')
    157                ->orderBy('subscriptions_count', 'desc')
    158                ->limit($limit)
    159                ->get();
    160        });
    161    }
    162    
    163    public function getUserSubscriptionHistory(User $user): Collection
    164    {
    165        return cache()->remember("user_subscriptions:{$user->id}", 600, function () use ($user) {
    166            return $user->subscriptions()
    167                ->with('plan')
    168                ->orderBy('created_at', 'desc')
    169                ->get();
    170        });
    171    }
    172    
    173    public function getSubscriptionMetrics(): array
    174    {
    175        return cache()->remember('subscription_metrics', 3600, function () {
    176            return [
    177                'total_active' => Subscription::where('status', SubscriptionStatus::ACTIVE)->count(),
    178                'total_revenue' => Subscription::sum('total_paid'),
    179                'monthly_growth' => $this->calculateMonthlyGrowth(),
    180                'churn_rate' => $this->calculateChurnRate(),
    181            ];
    182        });
    183    }
    184    
    185    private function calculateMonthlyGrowth(): float
    186    {
    187        $thisMonth = Subscription::whereMonth('created_at', now()->month)->count();
    188        $lastMonth = Subscription::whereMonth('created_at', now()->subMonth()->month)->count();
    189        
    190        return $lastMonth > 0 ? (($thisMonth - $lastMonth) / $lastMonth) * 100 : 0;
    191    }
    192    
    193    private function calculateChurnRate(): float
    194    {
    195        $totalCancelled = Subscription::where('status', SubscriptionStatus::CANCELLED)
    196            ->whereMonth('cancelled_at', now()->month)
    197            ->count();
    198            
    199        $totalActive = Subscription::where('status', SubscriptionStatus::ACTIVE)->count();
    200        
    201        return $totalActive > 0 ? ($totalCancelled / $totalActive) * 100 : 0;
    202    }
    203}
    

    This modern caching strategy provides organized cache management without relying on deprecated cache tags, while still maintaining automatic invalidation and performance benefits.

    #Testing Patterns for Confidence

    Comprehensive testing patterns ensure your scalable architecture remains reliable as it grows:

     1// Feature test for subscription creation
     2class CreateSubscriptionTest extends TestCase
     3{
     4    use RefreshDatabase;
     5    
     6    public function test_user_can_create_subscription_with_valid_payment()
     7    {
     8        Event::fake([SubscriptionCreated::class]);
     9        Queue::fake();
    10        
    11        $user = User::factory()->create();
    12        $plan = Plan::factory()->create(['price' => 2999]);
    13        
    14        $response = $this->actingAs($user)
    15            ->postJson(route('subscriptions.store'), [
    16                'plan_id' => $plan->id,
    17                'payment_method' => 'credit_card',
    18                'payment_token' => 'valid_token_123',
    19            ]);
    20        
    21        $response->assertStatus(201)
    22            ->assertJsonStructure([
    23                'success',
    24                'message',
    25                'data' => [
    26                    'id',
    27                    'status',
    28                    'plan',
    29                    'expires_at',
    30                    'can_cancel',
    31                ]
    32            ]);
    33        
    34        $this->assertDatabaseHas('subscriptions', [
    35            'user_id' => $user->id,
    36            'plan_id' => $plan->id,
    37            'status' => SubscriptionStatus::ACTIVE->value,
    38        ]);
    39        
    40        Event::assertDispatched(SubscriptionCreated::class, function ($event) use ($user) {
    41            return $event->subscription->user_id === $user->id;
    42        });
    43    }
    44    
    45    public function test_subscription_creation_fails_with_invalid_payment()
    46    {
    47        $user = User::factory()->create();
    48        $plan = Plan::factory()->create();
    49        
    50        $response = $this->actingAs($user)
    51            ->postJson(route('subscriptions.store'), [
    52                'plan_id' => $plan->id,
    53                'payment_method' => 'credit_card',
    54                'payment_token' => 'invalid_token',
    55            ]);
    56        
    57        $response->assertStatus(402)
    58            ->assertJson([
    59                'success' => false,
    60                'message' => 'Payment failed: Invalid payment token',
    61            ]);
    62        
    63        $this->assertDatabaseMissing('subscriptions', [
    64            'user_id' => $user->id,
    65            'plan_id' => $plan->id,
    66        ]);
    67    }
    68}
    69
    70// Unit test for business logic
    71class CreateSubscriptionActionTest extends TestCase
    72{
    73    use RefreshDatabase;
    74    
    75    public function test_creates_subscription_with_correct_expiration()
    76    {
    77        $user = User::factory()->create();
    78        $plan = Plan::factory()->create(['duration_days' => 30]);
    79        
    80        $dto = new SubscriptionDataDto(
    81            user_id: $user->id,
    82            plan_id: $plan->id,
    83            status: SubscriptionStatus::ACTIVE,
    84            expires_at: now()->addDays(30),
    85        );
    86        
    87        $action = CreateSubscriptionAction::instance();
    88        $subscription = $action->handle($dto);
    89        
    90        $this->assertInstanceOf(Subscription::class, $subscription);
    91        $this->assertEquals($user->id, $subscription->user_id);
    92        $this->assertEquals($plan->id, $subscription->plan_id);
    93        $this->assertEquals(SubscriptionStatus::ACTIVE, $subscription->status);
    94        $this->assertTrue($subscription->expires_at->isSameDay(now()->addDays(30)));
    95    }
    96}
    97
    98// Test factory with states
    99class SubscriptionFactory extends Factory
    100{
    101    public function definition(): array
    102    {
    103        return [
    104            'user_id' => User::factory(),
    105            'plan_id' => Plan::factory(),
    106            'status' => SubscriptionStatus::ACTIVE->value,
    107            'expires_at' => now()->addDays(30),
    108            'created_at' => now(),
    109        ];
    110    }
    111    
    112    public function expired(): static
    113    {
    114        return $this->state([
    115            'status' => SubscriptionStatus::EXPIRED->value,
    116            'expires_at' => now()->subDays(1),
    117        ]);
    118    }
    119    
    120    public function cancelled(): static
    121    {
    122        return $this->state([
    123            'status' => SubscriptionStatus::CANCELLED->value,
    124            'cancelled_at' => now(),
    125            'cancellation_reason' => 'User requested cancellation',
    126        ]);
    127    }
    128    
    129    public function expiringSoon(): static
    130    {
    131        return $this->state([
    132            'expires_at' => now()->addDays(3),
    133        ]);
    134    }
    135}
    136
    137// Architecture testing with Pest
    138arch('Actions should extend BaseAction')
    139    ->expect('App\Actions')
    140    ->toExtend('App\Actions\Contracts\BaseAction');
    141
    142arch('DTOs should extend BaseDto')
    143    ->expect('App\DTO')
    144    ->toExtend('App\DTO\BaseDto');
    145
    146arch('No debugging functions in production code')
    147    ->expect(['dd', 'dump', 'var_dump'])
    148    ->not->toBeUsed();
    

    These testing patterns provide confidence in your architecture while making it easy to verify that changes don't break existing functionality.

    #Real-World Implementation Patterns

    #Progressive Enhancement Strategy

    When implementing these patterns in existing applications, consider a progressive approach:

     1// Phase 1: Start with Actions for new features
     2class ModernFeatureController extends Controller
     3{
     4    public function store(Request $request, CreateFeatureAction $action)
     5    {
     6        // New features use actions immediately
     7        $feature = $action->handle($request->validated());
     8        return new FeatureResource($feature);
     9    }
    10}
    11
    12// Phase 2: Gradually refactor existing controllers
    13class LegacyFeatureController extends Controller
    14{
    15    public function update(Request $request, Feature $feature)
    16    {
    17        // Gradually extract to actions
    18        $action = UpdateFeatureAction::instance();
    19        $updatedFeature = $action->handle($feature, $request->validated());
    20        
    21        return new FeatureResource($updatedFeature);
    22    }
    23}
    24
    25// Phase 3: Implement consistent patterns across the application
    26trait HasModernPatterns
    27{
    28    protected function executeAction(string $actionClass, ...$parameters)
    29    {
    30        return app($actionClass)->handle(...$parameters);
    31    }
    32}
    

    #Environment-Specific Optimizations

     1// config/cache.php - Environment-aware cache configuration
     2return [
     3    'subscription_metrics_ttl' => env('APP_ENV') === 'production' ? 3600 : 60,
     4    'user_data_ttl' => env('APP_ENV') === 'production' ? 1800 : 30,
     5    'query_cache_enabled' => env('QUERY_CACHE_ENABLED', env('APP_ENV') === 'production'),
     6];
     7
     8// Conditional performance optimizations
     9class PerformanceOptimizedService
    10{
    11    public function getData(): array
    12    {
    13        if (app()->environment('production')) {
    14            return cache()->remember('expensive_data', 3600, fn() => $this->calculateExpensiveData());
    15        }
    16        
    17        // Skip caching in development for easier debugging
    18        return $this->calculateExpensiveData();
    19    }
    20}
    

    #Monitoring and Observability

     1// Performance monitoring helper
     2function monitorPerformance(string $operation, callable $callback)
     3{
     4    $startTime = microtime(true);
     5    $startMemory = memory_get_usage(true);
     6    
     7    $result = $callback();
     8    
     9    $executionTime = (microtime(true) - $startTime) * 1000;
    10    $memoryUsed = memory_get_usage(true) - $startMemory;
    11    
    12    Log::info("Performance: {$operation}", [
    13        'execution_time_ms' => round($executionTime, 2),
    14        'memory_used_mb' => round($memoryUsed / 1024 / 1024, 2),
    15    ]);
    16    
    17    return $result;
    18}
    19
    20// Usage in actions
    21class ExpensiveCalculationAction extends BaseAction
    22{
    23    public function handle(array $data): array
    24    {
    25        return monitorPerformance('expensive_calculation', function () use ($data) {
    26            // Complex calculation logic
    27            return $this->performCalculation($data);
    28        });
    29    }
    30}
    

    #Key Strengths and Benefits

    #Developer Experience Excellence

    Type Safety Everywhere: Enums, DTOs, and strict typing prevent entire categories of bugs before they reach production.

    Predictable Patterns: Once developers learn the action pattern, they can navigate any part of the application with confidence.

    Testing Confidence: Every business operation is easily testable in isolation.

    #Performance Optimization

    Intelligent Caching: Model-aware cache invalidation ensures data freshness while maximizing performance.

    Queue-Based Architecture: Background processing keeps the application responsive under heavy load.

    Optimized Queries: Pipeline filters ensure only necessary query constraints are applied.

    #Scalability and Maintenance

    Separation of Concerns: Each pattern addresses a specific architectural concern without overlap.

    Event-Driven Design: Loose coupling between components makes the system easy to extend and modify.

    Consistent Standards: Patterns provide guidelines that prevent architectural drift as teams grow.

    #Final Thoughts

    Building scalable Laravel applications isn't about using every advanced feature—it's about choosing the right patterns for your specific needs and implementing them consistently. The patterns discussed here have been battle-tested in production environments serving thousands of users and processing millions of requests.

    The key insight is that great architecture emerges from simple, well-implemented patterns rather than complex, over-engineered solutions. Start with the patterns that solve your immediate pain points: perhaps Actions for business logic organization, or Pipeline Filters for complex queries. Then gradually incorporate other patterns as your application's complexity grows.

    What makes these Laravel patterns exceptional is their respect for both developer productivity and application performance, proving that you don't have to sacrifice one for the other. Whether you're building your first scalable application or refactoring an existing monolith, these architectural decisions provide a solid foundation for sustainable growth.

    The difference between applications that scale gracefully and those that become maintenance nightmares lies in these foundational choices. Choose patterns that grow with your team and your user base, and your future self will thank you.


    Ready to implement these patterns in your Laravel application? Start small, be consistent, and let these battle-tested approaches guide your architectural decisions toward a more maintainable and scalable codebase.