πŸ“ Volume III: Advanced Architecture

🌊 Topic 12: Statelessness Doctrine

Your server must be killable at any moment. Design for failure.

"Your server is not special. It will die.
When it does, your users shouldn't notice.
If they do, you failed the Statelessness Doctrine."
⚠️ THE HORIZONTAL SCALING KILLER

The #1 reason Laravel apps can't scale horizontally? State stored on the server. Sessions in files. Images on local disk. Logs on local disk. Config in .env (not cached). When you try to run 10 servers behind a load balancer, each server has different data. Users get logged out randomly. Uploaded images disappear. Statelessness is the foundation of scalability.

πŸ”΄ The Problem: State Stored Locally

β”‚ β”‚ STATEFUL ARCHITECTURE (CANNOT SCALE) β”‚ ═══════════════════════════════════════════════════════════════════ β”‚ β”‚ Load Balancer β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β–Ό β–Ό β–Ό β–Ό β”‚ β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”‚ β”‚Serverβ”‚ β”‚Serverβ”‚ β”‚Serverβ”‚ β”‚Serverβ”‚ β”‚ β”‚ 1 β”‚ β”‚ 2 β”‚ β”‚ 3 β”‚ β”‚ 4 β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚Sessionβ”‚ β”‚Sessionβ”‚ β”‚Sessionβ”‚ β”‚Sessionβ”‚ β”‚ β”‚ Files β”‚ β”‚ Files β”‚ β”‚ Files β”‚ β”‚ Files β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚Uploadsβ”‚ β”‚Uploadsβ”‚ β”‚Uploadsβ”‚ β”‚Uploadsβ”‚ β”‚ β”‚(local)β”‚ β”‚(local)β”‚ β”‚(local)β”‚ β”‚(local)β”‚ β”‚ β””β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ PROBLEMS: β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β€’ User logs in on Server 1 β†’ Session saved on Server 1 β”‚ β”‚ β”‚ β€’ Next request goes to Server 2 β†’ No session β†’ User logged outβ”‚ β”‚ β”‚ β€’ User uploads avatar to Server 3 β†’ Image on Server 3 β”‚ β”‚ β”‚ β€’ Next request goes to Server 1 β†’ Avatar not found β†’ 404 β”‚ β”‚ β”‚ β€’ Server 2 dies β†’ All sessions on Server 2 are GONE forever β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
THE STATEFUL SINS (What NOT to do)

βœ… The Solution: Stateless Architecture

β”‚ β”‚ STATELESS ARCHITECTURE (SCALES HORIZONTALLY) β”‚ ═══════════════════════════════════════════════════════════════════ β”‚ β”‚ Load Balancer β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β–Ό β–Ό β–Ό β–Ό β”‚ β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”‚ β”‚Serverβ”‚ β”‚Serverβ”‚ β”‚Serverβ”‚ β”‚Serverβ”‚ β”‚ β”‚ 1 β”‚ β”‚ 2 β”‚ β”‚ 3 β”‚ β”‚ 4 β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ (No β”‚ β”‚ (No β”‚ β”‚ (No β”‚ β”‚ (No β”‚ β”‚ β”‚local β”‚ β”‚local β”‚ β”‚local β”‚ β”‚local β”‚ β”‚ β”‚state)β”‚ β”‚state)β”‚ β”‚state)β”‚ β”‚state)β”‚ β”‚ β””β”€β”€β”€β”¬β”€β”€β”˜ β””β”€β”€β”€β”¬β”€β”€β”˜ β””β”€β”€β”€β”¬β”€β”€β”˜ β””β”€β”€β”€β”¬β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Redis β”‚ β”‚ β”‚ β”‚ (Sessions β”‚ β”‚ β”‚ β”‚ & Cache) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ S3 / Object Storage β”‚ β”‚ β”‚ (Uploaded Files) β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ BENEFITS: β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β€’ Any server can handle any request β”‚ β”‚ β”‚ β€’ Add/remove servers anytime (auto-scaling) β”‚ β”‚ β”‚ β€’ Server dies? No problem β€” other servers take over β”‚ β”‚ β”‚ β€’ Zero-downtime deployments (drain one server at a time) β”‚ β”‚ β”‚ β€’ Global scale (Redis and S3 are shared across regions) β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
THE GOLDEN RULE OF STATELESSNESS

Every server must be able to die at any moment without affecting users. All state must live in external services (Redis, S3, Database).

πŸ“‹ The Statelessness Checklist

Component Stateful (BAD) Stateless (GOOD) Why
Sessions File driver (default) Redis driver Sessions must be shared across all servers
Cache File driver (default) Redis or Memcached Cache must be consistent across servers
Uploaded Files storage/app/public S3, R2, or GCS Files must be accessible from any server
Logs storage/logs (local) CloudWatch, ELK, Papertrail Centralized debugging across servers
Configuration Hardcoded or .env only Environment variables + config:cache Config changes without redeploy
Queue Workers Local php artisan queue:work Redis + Supervisor on dedicated workers Workers can run anywhere, scale independently
Scheduled Tasks Cron on one server php artisan schedule:run on all servers + mutex Prevents duplicate task execution

βš™οΈ Making Laravel Stateless (Step by Step)

1. Move Sessions to Redis

# .env
SESSION_DRIVER=redis
SESSION_CONNECTION=default

# config/session.php (already configured for Redis)

2. Move Cache to Redis

# .env
CACHE_DRIVER=redis

# config/cache.php
'default' => env('CACHE_DRIVER', 'redis'),

3. Move Files to S3

# .env
FILESYSTEM_DRIVER=s3
AWS_ACCESS_KEY_ID=your-key
AWS_SECRET_ACCESS_KEY=your-secret
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=your-bucket

# In your code
$path = Storage::disk('s3')->put('avatars', $request->file('avatar'));
$url = Storage::disk('s3')->url($path);

4. Move Logs to Centralized Service

# config/logging.php
'channels' => [
    'cloudwatch' => [
        'driver' => 'custom',
        'via' => \App\Logging\CloudWatchLogger::class,
    ],
],

# .env
LOG_CHANNEL=cloudwatch

5. Ensure Config is Cached

# After every deployment
php artisan config:cache

# Never use env() outside config files
// Wrong: env('APP_NAME')
// Right: config('app.name')

6. Handle Scheduled Tasks with Mutex

// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
    // With onOneServer(), tasks run only once across all servers
    $schedule->command('reports:generate')
        ->daily()
        ->onOneServer();  // ← Uses Redis/Cache for mutex
    
    $schedule->command('emails:send')
        ->hourly()
        ->onOneServer();
}

// For Laravel 8+, you need to set:
// config/schedule.php
'schedule_cache_driver' => 'redis',

πŸ”„ Horizontal Scaling: Adding More Servers

β”‚ β”‚ AUTO-SCALING WITH STATELESS SERVERS β”‚ ═══════════════════════════════════════════════════════════════════ β”‚ β”‚ Traffic increases (Black Friday) β”‚ β”‚ β”‚ β–Ό β”‚ Load Balancer detects high CPU β”‚ β”‚ β”‚ β–Ό β”‚ Auto-scaling group: Add 5 more servers β”‚ β”‚ β”‚ β–Ό β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ New servers boot, join the pool β”‚ β”‚ β”‚ β€’ No data to sync (stateless!) β”‚ β”‚ β”‚ β€’ Sessions already in Redis β”‚ β”‚ β”‚ β€’ Files already in S3 β”‚ β”‚ β”‚ β€’ Ready in 30 seconds β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β–Ό β”‚ Traffic handled. Users unaffected. β”‚ β”‚ ═══════════════════════════════════════════════════════════════════ β”‚ β”‚ ZERO-DOWNTIME DEPLOYMENT β”‚ ═══════════════════════════════════════════════════════════════════ β”‚ β”‚ Step 1: Add new servers with new code (in parallel) β”‚ Step 2: Wait for them to be ready β”‚ Step 3: Drain connections from old servers (no new requests) β”‚ Step 4: Remove old servers from load balancer β”‚ Step 5: Terminate old servers β”‚ β”‚ Result: Users never see a "down for maintenance" page. β”‚
REAL-WORLD EXAMPLE

Laravel Forge + AWS Auto-scaling Group + Load Balancer:

# Forge recipe for stateless servers
- 1 GB swap (no session files)
- Nginx + PHP-FPM
- .env with Redis/S3 credentials
- Deployment script:
    git pull
    composer install --no-dev --optimize-autoloader
    php artisan migrate --force
    php artisan config:cache
    php artisan route:cache
    php artisan view:cache
    php artisan queue:restart
    sudo systemctl reload php8.2-fpm

⚠️ The Downsides of Statelessness

COST
COMPLEXITY
BUT THE BENEFITS OUTWEIGH THE COSTS

For any application that expects growth, the ability to scale horizontally is worth the added complexity. Start stateless from day one. It's much harder to retrofit later.

πŸ” How to Check If Your App Is Stateless

THE ULTIMATE TEST

Kill a random server during peak traffic. If users notice, you have state somewhere.

Automated statelessness audit:

#!/bin/bash
# audit-statelessness.sh

echo "Checking for stateful patterns..."

# Check session driver
grep "SESSION_DRIVER=file" .env && echo "❌ Sessions in files! Use Redis."

# Check cache driver
grep "CACHE_DRIVER=file" .env && echo "❌ Cache in files! Use Redis."

# Check filesystem
grep "FILESYSTEM_DRIVER=local" .env && echo "❌ Files on local disk! Use S3."

# Check for env() in app code (outside config)
grep -r "env(" --include="*.php" --exclude-dir=vendor \
  --exclude-dir=config | grep -v "config/" && \
  echo "❌ env() found outside config/! Use config()."

# Check for hardcoded paths
grep -r "storage/app/public" --include="*.php" --exclude-dir=vendor && \
  echo "❌ Hardcoded storage path! Use Storage facade."

echo "Audit complete."
πŸ”΄ THE RULE: Run the audit before every deployment. Fix stateful violations immediately. Statelessness is not optional for production.

πŸ“ Topic 12 Summary: Statelessness Doctrine

Component Stateful (BAD) Stateless (GOOD)
Sessions File database Redis
Cache File database Redis Memcached
Files Local disk (storage/) S3 R2 GCS
Logs Local disk CloudWatch ELK Papertrail
Config .env + env() calls config:cache + config()
Scheduled Tasks Cron on single server onOneServer() + Redis mutex
πŸ“Œ THE RULE: Your server must be disposable. If you can't terminate it at any moment without user impact, you are not stateless. Fix sessions, files, logs, and cache first. Then scale horizontally with confidence.

Statelessness is the foundation of cloud-native applications. Start stateless from day one.
NEXT TOPIC PREVIEW

Topic 13: Event-Driven Architecture (EDA) β€” Stop making users wait. Dispatch events, return 202 Accepted, process in background. How to go from 500ms response time to 20ms.