Scalabilità nel backend: patterns e best practices per sistemi ad alto carico

Scalare un sistema da 1000 a 100.000 utenti simultanei non è solo questione di hardware. Richiede ripensare l’architettura, i pattern di accesso ai dati e la comunicazione asincrona.

Curva di scalabilità e i suoi colli di bottiglia

           Throughput (req/s)

         100k │     ╱─────── CPU-bound limit
              │    ╱
          10k │   ╱
              │  ╱
           1k │ ╱
              ├┼────────────────────→ Utenti simultanei
              0 1k   10k   100k   1M

Colli di bottiglia:
├─ 1-10k: I/O Database (queries lente)
├─ 10-100k: Memory & CPU (processing)
└─ 100k+: Network e coordinamento distribuito

Strategy 1: Caching Multi-livello

L1: Application Cache (In-Memory)

Request

┌─────────────────────┐
│ L1: Local Memory    │  ← HttpContext.Items
│ (Thread-safe dict)  │      Cache per request
└─────────────┬───────┘
              │ MISS
┌─────────────▼───────┐
│ L2: Distributed     │  ← Redis / Memcached
│ Cache (Shared)      │     Cache per istanza
└─────────────┬───────┘
              │ MISS
┌─────────────▼───────┐
│ L3: Database        │  ← SQL Server
│ (Source of Truth)   │     Persistente
└─────────────────────┘

Cache Invalidation Strategies

Time-based (TTL)

cache.Set(key, value, TimeSpan.FromMinutes(5));

Semplice ma rischia data stale.

Event-based

// Quando modifichiamo un prodotto
await cache.RemoveAsync($"product_{id}");
// Publish evento per altri servizi
await messageBus.PublishAsync(new ProductUpdated(id));

Più complesso, ma consistente.

Cache-Aside Pattern

Leggi da cache
  ├─ HIT: Return
  └─ MISS: Leggi DB → Salva in cache → Return

Write-Through Pattern

Scrivi DB
  └─ Scrivi cache (atomico)

Strategy 2: Database Optimization

Read Replicas

┌──────────────┐
│ Write Master │  (1 sola istanza)
│ (Autorità)   │
└────────┬─────┘
         │ Replica Stream
    ┌────┴──────┬──────┐
    ↓           ↓      ↓
┌────────┐ ┌────────┐ ┌────────┐
│ Read   │ │ Read   │ │ Read   │
│ Replica│ │Replica │ │Replica │  (N repliche)
└────────┘ └────────┘ └────────┘

App → Master (INSERT/UPDATE)
App → Read Replica (SELECT)

Database Sharding

Partizionare dati per ID o geolocalizzazione:

Shard Strategy: user_id % 4

User 1001 → Shard 1
User 1002 → Shard 2
User 1003 → Shard 3
User 1004 → Shard 0
User 1005 → Shard 1 (...)

┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐
│ Shard 0 │  │ Shard 1 │  │ Shard 2 │  │ Shard 3 │
│ Users:  │  │ Users:  │  │ Users:  │  │ Users:  │
│ 4,8,..  │  │ 1,5,..  │  │ 2,6,..  │  │ 3,7,..  │
└─────────┘  └─────────┘  └─────────┘  └─────────┘

Vantaggi:

  • ✅ Distribuzione carico
  • ✅ Scalabilità orizzontale

Sfide:

  • ❌ Cross-shard queries difficili
  • ❌ Ribilanciamento complesso

Strategy 3: Asynchronous Processing

Separa operazioni critiche da quelle “nice-to-have”:

Sync (veloce):
POST /orders → 
  ├─ Valida
  ├─ Crea record
  └─ Return 201 ✓ (50ms)

Async (background):
  ├─ Calcola inventario
  ├─ Invia email
  ├─ Analytics
  └─ (completate in background)

Total: User vede risposta in 50ms
       Job completa in 5s non-blocking

Job Queue Pattern

┌──────────┐
│ API      │
│ Crea job │
└─────┬────┘
      │ Enqueue
┌─────▼─────────────┐
│ Message Queue     │  (RabbitMQ / Azure Queue)
│ - SendEmail       │
│ - UpdateInventory │
│ - GenerateReport  │
└─────┬─────────────┘
      │ Dequeue
┌─────▼──────────────┐
│ Workers (N)        │
│ Processing jobs    │
└────────────────────┘

Strategy 4: Content Delivery

CDN (Content Delivery Network)

┌─────────────────────┐
│ Origin Server       │  (Backend .NET)
│ /images/photo.jpg   │
└──────────┬──────────┘
           │ Cached
    ┌──────┴──────┬─────────┐
    ↓             ↓         ↓
 [CDN Edge] [CDN Edge] [CDN Edge]
 (Milan)    (Frankfurt)(Amsterdam)

User in Rome    → CDN Milan (5ms)
User in Berlin  → CDN Frankfurt (3ms)
User in Paris   → CDN Frankfurt (8ms)

Compression

Response: 5MB JSON
  ├─ Gzip enabled
  └─ 1MB (80% riduzione)
  
Latency: 100ms → 20ms

Strategy 5: Monitoring & Observability

Key metrics da tracciare:

┌─ Latency
│  ├─ P50 (mediana)
│  ├─ P95 (quasi-worst case)
│  └─ P99 (worst case raro)

├─ Throughput
│  ├─ Req/sec
│  └─ Errori/sec

└─ Resource Usage
   ├─ CPU %
   ├─ Memory %
   ├─ Disk I/O
   └─ Network bandwidth

Roadmap di scalabilità

Fase 1 (1-10k users)
├─ Single server
├─ Caching (Redis)
└─ Database indexes

Fase 2 (10-100k users)
├─ Load Balancing
├─ Read Replicas
├─ Async jobs
└─ CDN per static

Fase 3 (100k-1M users)
├─ Database Sharding
├─ Microservizi
├─ Message Bus
└─ Advanced caching

Fase 4 (1M+ users)
├─ Global CDN
├─ Multi-region
├─ Event sourcing
└─ CQRS

La scalabilità non è “premature optimization”. È design intentionale dal principio: scegli patterns che permettono crescita senza rewrite completo.