Salta al contenuto principale

Pattern Architetturali Sostenibili: Storage, Cache e Batch

Pattern architetturali per software sostenibile: ottimizzazione storage, caching intelligente e scheduling batch carbon-aware.

Cosa ne pensi di questo articolo?

Pattern Architetturali Sostenibili: Storage, Cache e Batch
Green SoftwareFoto:Ruben BoekelooPexels

In Breve

Pattern architetturali per software sostenibile: ottimizzazione storage, caching intelligente e scheduling batch carbon-aware.

Pattern Architetturali Sostenibili: Storage, Cache e Batch

Le decisioni architetturali che prendiamo ogni giorno - come strutturiamo lo storage, come gestiamo la cache, come orchestriamo i batch job - determinano il carbon footprint del software in misura 10-100 volte superiore rispetto alle micro-ottimizzazioni del codice. Un'architettura mal progettata che accede al database ad ogni richiesta, non usa il caching, e processa i dati in tempo reale quando potrebbe farlo in batch, può consumare ordini di grandezza più energia di un'architettura ben progettata.

Secondo un'analisi del Green Software Foundation Impact Framework, le scelte architetturali relative a storage tiering, caching e scheduling batch rappresentano il 40-60% del potenziale di riduzione delle emissioni di un sistema software enterprise. Non si tratta solo di ottimizzazione tecnica: è una responsabilità professionale che ogni architetto e sviluppatore deve considerare parte integrante del proprio mestiere.

In questo nono articolo della serie Green Software esploreremo i pattern architetturali sostenibili più efficaci: dallo storage tiering intelligente alla cache carbon-aware, dal batch scheduling nelle finestre di energia verde all'API design sostenibile. Con esempi di codice reali e un caso studio completo che dimostra una riduzione del 45% del carbon footprint di un e-commerce da 1 milione di visite giornaliere.

Cosa Imparerai

  • Tiered storage (hot/warm/cold/archive) e politiche di data lifecycle per ridurre l'energia
  • Pattern di caching carbon-aware: CDN geo-intelligente, multi-level caching, invalidation ottimizzata
  • Batch processing nelle finestre di energia verde con Carbon Aware SDK
  • Right-sizing e auto-scaling down: come evitare il "idle computing" che spreca energia
  • Database pattern sostenibili: query optimization, materialized views, read replicas
  • Network efficiency: compressione, HTTP/3, edge computing
  • Frontend patterns sostenibili: lazy loading, image optimization, dark mode energy
  • API design sostenibile: pagination, field selection, GraphQL vs REST
  • Carbon monitoring con Prometheus, Grafana e SCI score per servizio
  • Caso studio e-commerce: -45% carbon footprint applicando tutti i pattern

Serie Green Software - 10 Articoli

# Titolo Focus Stato
1 Principi del Green Software Engineering GSF, SCI, 8 principi fondamentali Pubblicato
2 Misurare le Emissioni con CodeCarbon CodeCarbon, profiling energetico Python Pubblicato
3 Carbon Aware SDK: Scheduling Intelligente Spostamento temporale e geografico workload Pubblicato
4 Climatiq API: Dati di Emissione in Tempo Reale API emissioni, fattori di conversione Pubblicato
5 GreenOps: Infrastructure as Code Sostenibile Terraform green, spot instances, auto-scaling Pubblicato
6 AI e Carbon Footprint: Training Responsabile Modelli efficienti, LoRA, quantizzazione Pubblicato
7 Scope 3 nelle Pipeline Software Emissioni upstream/downstream, supply chain Pubblicato
8 Scope Modeling: Simulare l'Impatto Simulation, what-if analysis, roadmap verde Pubblicato
9 Pattern Architetturali Sostenibili Storage, Cache, Batch, API design Questo articolo
10 ESG, CSRD e Compliance per Software Team Reporting obbligatorio, audit trail, metriche ESG Prossimo

Storage Tiering: Il Dato Giusto nel Posto Giusto

Il primo grande spreco energetico nei sistemi moderni è lo storage non ottimizzato: dati acceduti raramente che risiedono su SSD NVMe ad alta performance, o peggio, in memoria RAM. Un SSD enterprise consuma fino a 6-10 Watt in idle e 25 Watt sotto carico. Storage su HDD consuma 5-8 Watt. Storage su tape o archivio a freddo consuma meno di 0.01 Watt per TB in idle.

La strategia di tiered storage consiste nel classificare i dati in base alla frequenza di accesso e spostarli automaticamente nel livello di storage energeticamente appropriato. Un'organizzazione media che implementa correttamente il tiered storage riduce i costi di storage del 40-70% e il relativo carbon footprint in proporzione simile.

Architettura a 4 Livelli di Storage

Livello Tipo Latenza Costo/TB/mese Energia/TB Uso Tipico
Hot NVMe SSD / Redis < 1ms $200-500 Alto (25W) Dati attivi ultimi 30 giorni
Warm SSD Standard / S3 Standard 1-10ms $20-50 Medio (8W) Dati 1-12 mesi, accesso settimanale
Cold HDD / S3 IA / GCS Nearline 50-250ms $4-10 Basso (3W) Dati 1-7 anni, accesso mensile
Archive Glacier / GCS Archive / Tape 1-12 ore $0.40-1 Minimo (<0.01W) Dati >7 anni, compliance, backup

Implementare Politiche di Data Lifecycle Automatiche

Il cuore del tiered storage è l'automazione delle politiche di lifecycle. Invece di gestire manualmente lo spostamento dei dati, definiamo regole che il sistema applica automaticamente in base all'età e alla frequenza di accesso dei dati.

Python - Data lifecycle manager con metriche carbon
from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import Enum
from typing import Optional
import boto3
import logging

logger = logging.getLogger(__name__)

class StorageTier(Enum):
    HOT = "hot"
    WARM = "warm"
    COLD = "cold"
    ARCHIVE = "archive"

# Energia media per operazione per tier (Wh per GB spostato/acceduto)
TIER_ENERGY_PROFILE = {
    StorageTier.HOT: {"idle_w_per_tb": 25, "access_wh_per_gb": 0.002},
    StorageTier.WARM: {"idle_w_per_tb": 8, "access_wh_per_gb": 0.0008},
    StorageTier.COLD: {"idle_w_per_tb": 3, "access_wh_per_gb": 0.0003},
    StorageTier.ARCHIVE: {"idle_w_per_tb": 0.01, "access_wh_per_gb": 0.00001},
}

@dataclass(frozen=True)
class LifecyclePolicy:
    """Immutabile: definisce le soglie di transizione tra tier."""
    hot_to_warm_days: int = 30
    warm_to_cold_days: int = 90
    cold_to_archive_days: int = 365
    delete_after_days: Optional[int] = None  # None = mantieni forever

@dataclass(frozen=True)
class DataAsset:
    """Rappresenta un asset dati con le sue metriche."""
    key: str
    size_gb: float
    created_at: datetime
    last_accessed: datetime
    current_tier: StorageTier
    access_count_30d: int

class DataLifecycleManager:
    """Gestisce il lifecycle dei dati verso tier energeticamente ottimali."""

    def __init__(self, policy: LifecyclePolicy, s3_client=None):
        self._policy = policy
        self._s3 = s3_client or boto3.client("s3")

    def evaluate_tier_transition(self, asset: DataAsset) -> Optional[StorageTier]:
        """
        Determina se un asset deve essere spostato.
        Ritorna il nuovo tier o None se nessun cambio necessario.
        """
        age_days = (datetime.utcnow() - asset.created_at).days
        days_since_access = (datetime.utcnow() - asset.last_accessed).days

        # Regola 1: archivio per dati molto vecchi
        if age_days > self._policy.cold_to_archive_days:
            if asset.current_tier != StorageTier.ARCHIVE:
                return StorageTier.ARCHIVE

        # Regola 2: cold per dati accessati raramente
        elif age_days > self._policy.warm_to_cold_days and asset.access_count_30d < 2:
            if asset.current_tier in (StorageTier.HOT, StorageTier.WARM):
                return StorageTier.COLD

        # Regola 3: warm per dati maturi con accesso moderato
        elif age_days > self._policy.hot_to_warm_days and asset.access_count_30d < 10:
            if asset.current_tier == StorageTier.HOT:
                return StorageTier.WARM

        return None  # Nessun cambio necessario

    def calculate_carbon_savings(
        self,
        asset: DataAsset,
        target_tier: StorageTier,
        carbon_intensity_g_kwh: float = 350  # Media europea 2025
    ) -> dict:
        """Stima il risparmio CO2 mensile dal cambio di tier."""
        current_profile = TIER_ENERGY_PROFILE[asset.current_tier]
        target_profile = TIER_ENERGY_PROFILE[target_tier]

        # Energia idle mensile (30 giorni * 24 ore)
        current_idle_wh = (current_profile["idle_w_per_tb"] * asset.size_gb / 1000) * 720
        target_idle_wh = (target_profile["idle_w_per_tb"] * asset.size_gb / 1000) * 720

        savings_wh = current_idle_wh - target_idle_wh
        savings_kwh = savings_wh / 1000
        savings_co2_g = savings_kwh * carbon_intensity_g_kwh

        return {
            "monthly_savings_kwh": round(savings_kwh, 4),
            "monthly_savings_co2_g": round(savings_co2_g, 2),
            "annual_savings_co2_kg": round(savings_co2_g * 12 / 1000, 3),
        }

    def execute_transition(self, asset: DataAsset, target_tier: StorageTier, bucket: str) -> dict:
        """Sposta l'asset al nuovo tier su S3 con storage class appropriata."""
        storage_class_map = {
            StorageTier.WARM: "STANDARD_IA",
            StorageTier.COLD: "GLACIER_IR",
            StorageTier.ARCHIVE: "DEEP_ARCHIVE",
        }

        storage_class = storage_class_map.get(target_tier, "STANDARD")
        savings = self.calculate_carbon_savings(asset, target_tier)

        try:
            # Copia con nuova storage class (immutabile: crea nuovo oggetto S3)
            self._s3.copy_object(
                CopySource={"Bucket": bucket, "Key": asset.key},
                Bucket=bucket,
                Key=asset.key,
                StorageClass=storage_class,
                MetadataDirective="COPY",
            )
            logger.info(
                "Tier transition: %s -> %s | CO2 saved: %.2fg/month",
                asset.current_tier.value, target_tier.value,
                savings["monthly_savings_co2_g"]
            )
            return {"success": True, "savings": savings}
        except Exception as e:
            logger.error("Transition failed for %s: %s", asset.key, e)
            return {"success": False, "error": str(e)}


# AWS S3 Lifecycle Policy come JSON (alternativa Infrastructure-as-Code)
S3_LIFECYCLE_POLICY = {
    "Rules": [
        {
            "ID": "GreenDataLifecycle",
            "Status": "Enabled",
            "Transitions": [
                {"Days": 30, "StorageClass": "STANDARD_IA"},
                {"Days": 90, "StorageClass": "GLACIER_IR"},
                {"Days": 365, "StorageClass": "DEEP_ARCHIVE"},
            ],
            "Expiration": {"Days": 2555},  # 7 anni poi elimina
        }
    ]
}

Compressione: Il Pattern Più Sottovalutato

La compressione dei dati riduce contemporaneamente il consumo di storage (meno byte da mantenere) e il traffico di rete (meno byte da trasferire). Il rapporto energia risparmiata / energia spesa per la compressione è tipicamente 10:1 a 100:1, rendendolo uno dei pattern più energeticamente vantaggiosi.

Python - Scelta algoritmo di compressione ottimale per efficienza carbonica
import zlib
import lz4.frame
import zstandard as zstd
import time
from dataclasses import dataclass
from typing import Callable

@dataclass(frozen=True)
class CompressionProfile:
    """Profilo immutabile di un algoritmo di compressione."""
    name: str
    compress_fn: Callable[[bytes], bytes]
    decompress_fn: Callable[[bytes], bytes]
    cpu_intensity: float   # 1.0 = baseline, <1 = meno CPU, >1 = più CPU
    best_for: str

def benchmark_compression(data: bytes, profile: CompressionProfile) -> dict:
    """Misura efficienza energetica di un algoritmo su dati reali."""
    # Compressione
    start = time.perf_counter()
    compressed = profile.compress_fn(data)
    compress_ms = (time.perf_counter() - start) * 1000

    # Decompressione
    start = time.perf_counter()
    profile.decompress_fn(compressed)
    decompress_ms = (time.perf_counter() - start) * 1000

    ratio = len(data) / len(compressed)
    # Energia stimata: CPU_time * intensità * 0.001 Wh per ms di CPU
    compress_energy_mwh = compress_ms * profile.cpu_intensity * 0.001
    storage_savings_pct = (1 - 1/ratio) * 100

    return {
        "algorithm": profile.name,
        "ratio": round(ratio, 2),
        "storage_savings_pct": round(storage_savings_pct, 1),
        "compress_ms": round(compress_ms, 2),
        "decompress_ms": round(decompress_ms, 2),
        "compress_energy_mwh": round(compress_energy_mwh, 4),
        "best_for": profile.best_for,
    }

# Profili degli algoritmi principali
COMPRESSION_PROFILES = [
    CompressionProfile(
        name="zlib-6",
        compress_fn=lambda d: zlib.compress(d, level=6),
        decompress_fn=zlib.decompress,
        cpu_intensity=1.0,
        best_for="Compatibilità universale, HTTP responses"
    ),
    CompressionProfile(
        name="lz4",
        compress_fn=lz4.frame.compress,
        decompress_fn=lz4.frame.decompress,
        cpu_intensity=0.15,  # Molto veloce, meno CPU
        best_for="Stream real-time, alta frequenza di accesso"
    ),
    CompressionProfile(
        name="zstd-3",
        compress_fn=lambda d: zstd.ZstdCompressor(level=3).compress(d),
        decompress_fn=lambda d: zstd.ZstdDecompressor().decompress(d),
        cpu_intensity=0.4,
        best_for="Bilanciamento ottimale ratio/velocità (raccomandato)"
    ),
    CompressionProfile(
        name="zstd-19",
        compress_fn=lambda d: zstd.ZstdCompressor(level=19).compress(d),
        decompress_fn=lambda d: zstd.ZstdDecompressor().decompress(d),
        cpu_intensity=2.5,  # Alta CPU per compressione massima
        best_for="Archivio cold/archive, dati rari, batch notturno"
    ),
]

# Regola pratica per scelta del livello di compressione:
# HOT tier  -> lz4 (latenza minima, decompress velocissimo)
# WARM tier -> zstd-3 (bilanciamento ottimale)
# COLD tier -> zstd-9 (ratio migliore, latenza tollerabile)
# ARCHIVE   -> zstd-19 o brotli-11 (massimo risparmio storage)

Caching Carbon-Aware: Servire di Più Computando di Meno

Il caching è probabilmente il pattern più potente per ridurre le emissioni software: ogni cache hit elimina completamente il consumo energetico del calcolo corrispondente. Un sistema con cache hit rate del 90% esegue solo 1/10 delle computazioni rispetto a uno senza cache, con un risparmio energetico proporzionale.

Ma il "caching carbon-aware" va oltre la semplice ottimizzazione delle performance: considera l'intensità carbonica della grid per decidere cosa precaricare, per quanto tempo mantenere i dati in cache, e quando eseguire le operazioni di cache warming.

Architettura Multi-Level Cache

Livelli di Cache e Impatto Energetico

Livello Tecnologia Latenza Energia per Hit Energia Risparmiata vs DB
L1: In-Process HashMap, LRU in RAM < 0.1ms ~0.001 mWh 99.9% risparmio
L2: Distributed Redis, Memcached 0.1-1ms ~0.01 mWh 99% risparmio
L3: CDN Edge CloudFront, Fastly, CF 1-20ms ~0.05 mWh 95% risparmio
DB Query PostgreSQL, MySQL 5-100ms ~1-10 mWh - baseline
TypeScript - Multi-level cache con carbon-aware TTL
import Redis from "ioredis";

interface CacheEntry<T> {
  readonly data: T;
  readonly cachedAt: number;
  readonly ttlMs: number;
  readonly carbonIntensityAtCache: number;  // gCO2/kWh quando cacheato
}

interface CarbonAwareCacheConfig {
  readonly l1MaxEntries: number;
  readonly l1DefaultTtlMs: number;
  readonly l2DefaultTtlMs: number;
  readonly lowCarbonThreshold: number;      // gCO2/kWh
  readonly highCarbonTtlMultiplier: number; // TTL più lungo quando carbon e alto
}

const DEFAULT_CONFIG: CarbonAwareCacheConfig = {
  l1MaxEntries: 1000,
  l1DefaultTtlMs: 60_000,      // 1 minuto L1
  l2DefaultTtlMs: 300_000,     // 5 minuti L2
  lowCarbonThreshold: 200,     // <200 gCO2/kWh = energia verde
  highCarbonTtlMultiplier: 3,  // TTL 3x più lungo su energia sporca
};

class CarbonAwareMultiLevelCache<T> {
  private readonly l1 = new Map<string, CacheEntry<T>>();
  private readonly config: CarbonAwareCacheConfig;
  private readonly redis: Redis;
  private currentCarbonIntensity = 350; // Default, aggiornato periodicamente

  // Metriche immutabili per reporting
  private readonly metrics = {
    l1Hits: 0,
    l2Hits: 0,
    misses: 0,
    carbonSavedGrams: 0,
  };

  constructor(redisClient: Redis, config: Partial<CarbonAwareCacheConfig> = {}) {
    this.redis = redisClient;
    this.config = { ...DEFAULT_CONFIG, ...config };
  }

  async get(key: string): Promise<T | null> {
    // L1: memoria locale (nessuna I/O, minima energia)
    const l1Entry = this.l1.get(key);
    if (l1Entry && !this.isExpired(l1Entry)) {
      this.metrics.l1Hits++;
      this.metrics.carbonSavedGrams += 0.005; // ~5mg CO2 risparmiati vs DB
      return l1Entry.data;
    }

    // L2: Redis distribuito
    try {
      const raw = await this.redis.get(key);
      if (raw) {
        const entry: CacheEntry<T> = JSON.parse(raw);
        if (!this.isExpired(entry)) {
          // Promuovi in L1
          this.setL1(key, entry.data, this.config.l1DefaultTtlMs);
          this.metrics.l2Hits++;
          this.metrics.carbonSavedGrams += 0.003; // ~3mg CO2 vs DB
          return entry.data;
        }
      }
    } catch (err) {
      console.warn("L2 cache read failed, fallback to source:", err);
    }

    this.metrics.misses++;
    return null;
  }

  async set(key: string, data: T): Promise<void> {
    // TTL adattivo in base all'intensità carbonica corrente
    // Quando l'energia e verde (low carbon), TTL più breve e accettabile
    // Quando l'energia e "sporca" (high carbon), TTL più lungo per ridurre ricalcoli
    const isHighCarbon = this.currentCarbonIntensity > this.config.lowCarbonThreshold;
    const ttlMultiplier = isHighCarbon ? this.config.highCarbonTtlMultiplier : 1;

    const l1TtlMs = this.config.l1DefaultTtlMs * ttlMultiplier;
    const l2TtlMs = this.config.l2DefaultTtlMs * ttlMultiplier;

    this.setL1(key, data, l1TtlMs);

    // Scrivi in Redis in modo non bloccante
    const entry: CacheEntry<T> = {
      data,
      cachedAt: Date.now(),
      ttlMs: l2TtlMs,
      carbonIntensityAtCache: this.currentCarbonIntensity,
    };

    await this.redis.set(key, JSON.stringify(entry), "PX", l2TtlMs);
  }

  private setL1(key: string, data: T, ttlMs: number): void {
    // Evict LRU se pieno
    if (this.l1.size >= this.config.l1MaxEntries) {
      const firstKey = this.l1.keys().next().value;
      if (firstKey) this.l1.delete(firstKey);
    }
    this.l1.set(key, {
      data,
      cachedAt: Date.now(),
      ttlMs,
      carbonIntensityAtCache: this.currentCarbonIntensity,
    });
  }

  private isExpired(entry: CacheEntry<T>): boolean {
    return Date.now() - entry.cachedAt > entry.ttlMs;
  }

  updateCarbonIntensity(intensityGCO2PerKWh: number): void {
    this.currentCarbonIntensity = intensityGCO2PerKWh;
  }

  getMetrics(): Readonly<typeof this.metrics> {
    const total = this.metrics.l1Hits + this.metrics.l2Hits + this.metrics.misses;
    return {
      ...this.metrics,
      hitRate: total ? ((this.metrics.l1Hits + this.metrics.l2Hits) / total * 100).toFixed(1) + "%" : "N/A",
    } as any;
  }
}

CDN Carbon-Aware: Servire dall'Edge Verde

Un CDN come Cloudflare o Fastly ha edge node in decine di region con intensità carbonica molto diversa. Instradare il traffico verso gli edge con energia più verde, quando la latenza lo permette, può ridurre le emissioni del serving del 20-40%.

TypeScript - Cache invalidation carbon-aware con tag-based purging
// Cache invalidation: uno dei problemi più difficili del software
// Pattern carbon-aware: invalida in batch durante finestre di bassa carbon intensity

interface InvalidationJob {
  readonly tags: readonly string[];
  readonly priority: "immediate" | "carbon-optimal" | "batch-night";
  readonly scheduledAt: Date;
  readonly maxDelayMs: number;
}

class CarbonAwareCacheInvalidator {
  private readonly pendingJobs: InvalidationJob[] = [];
  private readonly carbonAwareSdk: any; // Carbon Aware SDK instance

  async scheduleInvalidation(
    tags: string[],
    priority: InvalidationJob["priority"] = "carbon-optimal",
    maxDelayMs: number = 3_600_000 // 1 ora di tolleranza
  ): Promise<{ jobId: string; scheduledFor: Date }> {

    if (priority === "immediate") {
      await this.executePurge(tags);
      return { jobId: crypto.randomUUID(), scheduledFor: new Date() };
    }

    // Trova la finestra con minore carbon intensity nelle prossime maxDelayMs
    const optimalTime = await this.findGreenWindow(maxDelayMs);

    const job: InvalidationJob = {
      tags: Object.freeze(tags),
      priority,
      scheduledAt: optimalTime,
      maxDelayMs,
    };

    // Immutabile: non mutiamo pendingJobs esistenti, aggiungiamo nuovo
    this.pendingJobs.push(job);

    return {
      jobId: crypto.randomUUID(),
      scheduledFor: optimalTime,
    };
  }

  private async findGreenWindow(maxDelayMs: number): Promise<Date> {
    const windowEnd = new Date(Date.now() + maxDelayMs);

    try {
      // Carbon Aware SDK: trova il momento con minore intensità carbonica
      const forecast = await this.carbonAwareSdk.getForecast({
        location: "westeurope",
        start: new Date(),
        end: windowEnd,
        duration: 15, // Job richiede ~15 minuti
      });

      return new Date(forecast.optimalWindow.start);
    } catch {
      // Fallback: esegui immediatamente se il forecast non e disponibile
      return new Date();
    }
  }

  private async executePurge(tags: string[]): Promise<void> {
    // Cloudflare Cache Tag Purge API
    await fetch("https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache", {
      method: "POST",
      headers: {
        "Authorization": "Bearer CF_TOKEN",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ tags }),
    });
  }
}

Batch Processing Carbon-Aware: Lavora Quando l'Energia È Verde

Il batch processing è il candidato ideale per la temporal shifting, il principio che consiste nello spostare i carichi di lavoro flessibili nei momenti in cui l'intensità carbonica della rete elettrica è più bassa. Un sistema che fa girare i propri batch notturni quando l'eolico e il solare producono surplus energetico può ridurre le emissioni associate del 30-70% rispetto a un'esecuzione a orario fisso.

Il Paradosso del Batch alle 02:00

Molti sistemi schedulano i batch alle 02:00 "perché c'è meno traffico". Ma in Europa la notte non è sempre il momento con minore carbon intensity: in molte regioni il sole solare e assente la notte e il vento varia. In alcune zone, le ore 10-14 (solare al picco) o le ore 02-06 (vento costante) hanno intensità carbonica molto più bassa. Usare i dati reali del Carbon Aware SDK invece di rule-of-thumb temporali.

Python - Carbon-aware batch scheduler con Celery e Carbon Aware SDK
from celery import Celery
from celery.schedules import crontab
from datetime import datetime, timedelta
from typing import Optional, NamedTuple
import httpx
import asyncio
import logging

logger = logging.getLogger(__name__)

app = Celery("green_batch", broker="redis://localhost:6379/0")

class GreenWindow(NamedTuple):
    """Finestra di esecuzione ottimale per carbon footprint."""
    start: datetime
    end: datetime
    carbon_intensity_g_kwh: float
    savings_pct_vs_now: float

class CarbonAwareBatchScheduler:
    """Schedula batch job nelle finestre a minore carbon intensity."""

    BASE_CARBON_API = "https://api.electricitymap.org/v3"

    def __init__(self, api_token: str, location: str = "IT"):
        self._token = api_token
        self._location = location

    async def get_optimal_window(
        self,
        job_duration_minutes: int,
        max_delay_hours: int = 12,
        min_savings_pct: float = 15.0
    ) -> Optional[GreenWindow]:
        """
        Trova la finestra ottimale per eseguire un batch job.
        Ritorna None se nessuna finestra con risparmio sufficiente trovata.
        """
        headers = {"auth-token": self._token}

        async with httpx.AsyncClient() as client:
            # Intensità carbonica attuale
            current_resp = await client.get(
                f"{self.BASE_CARBON_API}/carbon-intensity/latest",
                params={"zone": self._location},
                headers=headers
            )
            current_data = current_resp.json()
            current_intensity = current_data["carbonIntensity"]

            # Forecast delle prossime ore
            forecast_resp = await client.get(
                f"{self.BASE_CARBON_API}/carbon-intensity/forecast",
                params={"zone": self._location},
                headers=headers
            )
            forecast = forecast_resp.json()

        now = datetime.utcnow()
        deadline = now + timedelta(hours=max_delay_hours)

        best_window: Optional[GreenWindow] = None
        min_intensity = current_intensity

        for slot in forecast["forecast"]:
            slot_time = datetime.fromisoformat(slot["datetime"].replace("Z", "+00:00"))
            slot_time = slot_time.replace(tzinfo=None)

            if slot_time < now or slot_time > deadline:
                continue

            intensity = slot["carbonIntensity"]
            if intensity < min_intensity:
                min_intensity = intensity
                savings_pct = (current_intensity - intensity) / current_intensity * 100

                if savings_pct >= min_savings_pct:
                    best_window = GreenWindow(
                        start=slot_time,
                        end=slot_time + timedelta(minutes=job_duration_minutes),
                        carbon_intensity_g_kwh=intensity,
                        savings_pct_vs_now=round(savings_pct, 1),
                    )

        return best_window

    async def schedule_green(
        self,
        task_name: str,
        job_duration_minutes: int,
        task_kwargs: dict,
        max_delay_hours: int = 12
    ) -> dict:
        """Schedula un Celery task nella finestra più verde."""
        window = await self.get_optimal_window(
            job_duration_minutes=job_duration_minutes,
            max_delay_hours=max_delay_hours
        )

        if window:
            delay_seconds = (window.start - datetime.utcnow()).total_seconds()
            task = app.send_task(
                task_name,
                kwargs=task_kwargs,
                countdown=max(0, int(delay_seconds))
            )
            logger.info(
                "Scheduled '%s' for %s (%.1f%% CO2 savings, %.0fg/kWh)",
                task_name, window.start.isoformat(),
                window.savings_pct_vs_now, window.carbon_intensity_g_kwh
            )
            return {
                "task_id": task.id,
                "scheduled_for": window.start.isoformat(),
                "carbon_intensity": window.carbon_intensity_g_kwh,
                "co2_savings_pct": window.savings_pct_vs_now,
            }
        else:
            # Nessuna finestra verde disponibile: esegui ora
            task = app.send_task(task_name, kwargs=task_kwargs)
            logger.warning("No green window found for '%s', executing immediately", task_name)
            return {"task_id": task.id, "scheduled_for": "now", "co2_savings_pct": 0}


# Definizione dei task Celery con metriche carbon
@app.task(name="batch.nightly_report", bind=True)
def nightly_report_batch(self, report_date: str) -> dict:
    """
    Genera report notturni. Non time-critical: ideale per temporal shifting.
    Carbon savings tipici: 20-60% spostando dalle 02:00 alla finestra verde.
    """
    logger.info("Generating report for %s (carbon-optimal execution)", report_date)
    # ... logica report
    return {"status": "completed", "report_date": report_date}

@app.task(name="batch.data_sync", bind=True)
def data_sync_batch(self, source: str) -> dict:
    """
    Sincronizzazione dati tra sistemi. Tollerante a ritardi di alcune ore.
    """
    # ... logica sync
    return {"status": "synced", "source": source}

# Scheduler che usa carbon-awareness invece di crontab fisso
async def schedule_nightly_jobs():
    scheduler = CarbonAwareBatchScheduler(
        api_token="your_electricity_maps_token",
        location="IT"  # Italia
    )

    # Report: accetta fino a 12h di ritardo
    await scheduler.schedule_green(
        task_name="batch.nightly_report",
        job_duration_minutes=45,
        task_kwargs={"report_date": datetime.now().strftime("%Y-%m-%d")},
        max_delay_hours=12
    )

    # Data sync: accetta fino a 6h di ritardo
    await scheduler.schedule_green(
        task_name="batch.data_sync",
        job_duration_minutes=20,
        task_kwargs={"source": "salesforce"},
        max_delay_hours=6
    )

Right-Sizing e Auto-Scaling Down: Fermare lo Spreco Invisibile

Uno studio di Gartner del 2025 stima che il 35-40% delle risorse cloud enterprise sono over-provisioned: server che girano al 10-15% di CPU, database con 90% di memoria inutilizzata, Lambda functions con 3GB di RAM allocata quando ne bastano 256MB. Questo "idle computing" è puro spreco energetico.

Il right-sizing sistematico - ridurre le risorse al minimo necessario per soddisfare i requisiti di performance - è spesso il singolo intervento con il miglior ROI energetico. Non richiede riscrittura del codice: è una questione di configurazione e monitoraggio.

Strategie di Right-Sizing per Carbon Reduction

Strategia Risparmio Tipico Complessità Rischio
Ridurre size istanze over-provisioned 20-40% Bassa Basso (rollback facile)
Auto-scaling down aggressivo 30-60% Media Medio (latenza cold start)
Serverless per workload intermittenti 50-90% Alta Medio (cold start, vendor lock-in)
Spot/Preemptible instances per batch 60-80% Alta Alto (interruzioni)
Schedule shutdown fuori orario (dev/staging) 40-70% Bassa Nullo (ambienti non-prod)
Python - Auto-scaling down carbon-aware con AWS Auto Scaling
import boto3
from datetime import datetime
from dataclasses import dataclass
from typing import Sequence

@dataclass(frozen=True)
class ScalingPolicy:
    """Politica di scaling immutabile con carbon awareness."""
    min_capacity: int
    max_capacity: int
    target_cpu_pct: float
    scale_in_cooldown_sec: int
    green_hour_min_capacity: int  # capacità minima durante ore verdi
    low_traffic_scale_in_factor: float  # Fattore aggressivo in ore basse traffico

def create_carbon_aware_scaling_policies(
    asg_name: str,
    policy: ScalingPolicy,
    region: str = "eu-west-1"
) -> dict:
    """
    Configura Auto Scaling Group con politiche carbon-aware.
    Scale-in più aggressivo durante ore di basso traffico (di notte)
    dove l'energia potrebbe essere più verde E il traffico e basso.
    """
    autoscaling = boto3.client("autoscaling", region_name=region)

    # Policy principale: target tracking su CPU
    main_policy = autoscaling.put_scaling_policy(
        AutoScalingGroupName=asg_name,
        PolicyName=f"{asg_name}-carbon-aware-cpu",
        PolicyType="TargetTrackingScaling",
        TargetTrackingConfiguration={
            "PredefinedMetricSpecification": {
                "PredefinedMetricType": "ASGAverageCPUUtilization"
            },
            "TargetValue": policy.target_cpu_pct,
            "ScaleInCooldown": policy.scale_in_cooldown_sec,
            "ScaleOutCooldown": 120,
            "DisableScaleIn": False,
        }
    )

    # Scheduled action: scale down notturno aggressivo
    # Combina basso traffico + potenziale energia verde
    autoscaling.put_scheduled_update_group_action(
        AutoScalingGroupName=asg_name,
        ScheduledActionName=f"{asg_name}-night-scaledown",
        Recurrence="0 22 * * *",  # Ogni giorno alle 22:00 UTC
        MinSize=policy.green_hour_min_capacity,
        MaxSize=policy.max_capacity,
        DesiredCapacity=policy.green_hour_min_capacity,
    )

    # Scheduled action: scale up mattutino prima del traffico
    autoscaling.put_scheduled_update_group_action(
        AutoScalingGroupName=asg_name,
        ScheduledActionName=f"{asg_name}-morning-scaleup",
        Recurrence="0 7 * * MON-FRI",  # Lun-Ven alle 07:00 UTC
        MinSize=policy.min_capacity,
        MaxSize=policy.max_capacity,
        DesiredCapacity=policy.min_capacity + 2,  # Pre-warm prima del traffico
    )

    return {
        "asg_name": asg_name,
        "main_policy_arn": main_policy["PolicyARN"],
        "estimated_monthly_co2_reduction_pct": 35,  # Tipico per questo pattern
    }


# Lambda Right-sizing: trova la memoria ottimale
# Principio: RAM in eccesso = costo carbonio inutile
def optimize_lambda_memory(function_name: str, region: str = "eu-west-1") -> dict:
    """
    Analizza l'utilizzo memoria di una Lambda e suggerisce il right-size.
    AWS Lambda Power Tuning (tool open source) automatizza questo processo.
    """
    lambda_client = boto3.client("lambda", region_name=region)
    cloudwatch = boto3.client("cloudwatch", region_name=region)

    # Recupera configurazione attuale
    config = lambda_client.get_function_configuration(FunctionName=function_name)
    current_memory_mb = config["MemorySize"]

    # Recupera metriche CloudWatch: max memory used negli ultimi 7 giorni
    # (in produzione usare AWS Lambda Power Tuning per analisi completa)
    metrics = cloudwatch.get_metric_statistics(
        Namespace="AWS/Lambda",
        MetricName="MaxMemoryUsed",
        Dimensions=[{"Name": "FunctionName", "Value": function_name}],
        StartTime=datetime.utcnow().replace(hour=0, minute=0) - __import__("datetime").timedelta(days=7),
        EndTime=datetime.utcnow(),
        Period=86400,
        Statistics=["Maximum"],
    )

    if not metrics["Datapoints"]:
        return {"status": "insufficient_data"}

    max_used_mb = max(dp["Maximum"] for dp in metrics["Datapoints"])
    # Buffer di sicurezza: 30% sopra il massimo osservato
    recommended_mb = min(int(max_used_mb * 1.3 / 64 + 1) * 64, 10240)

    co2_reduction_pct = max(0, (current_memory_mb - recommended_mb) / current_memory_mb * 100)

    return {
        "current_memory_mb": current_memory_mb,
        "max_observed_mb": int(max_used_mb),
        "recommended_mb": recommended_mb,
        "potential_co2_reduction_pct": round(co2_reduction_pct, 1),
        "annual_cost_savings_usd": (current_memory_mb - recommended_mb) / 1024 * 0.0000166667 * 3_600_000 * 12,
    }

Database Pattern Sostenibili: Meno Query, Meno Carbon

Il database è spesso il componente con il maggior consumo energetico di un sistema enterprise. Ogni query comporta I/O su disco, allocazione di buffer in RAM, cicli CPU per parsing, planning e execution. Ottimizzare le query non è solo una questione di performance: è una riduzione diretta delle emissioni.

Pattern 1: Materialized Views per Ridurre Ricalcoli

Le materialized views sono il pattern più efficace per eliminare query aggregate costose che vengono rieseguite continuamente. Invece di ricalcolare SUM, COUNT, JOIN complessi ad ogni richiesta, si precalcola il risultato e lo si aggiorna periodicamente o tramite trigger.

SQL (PostgreSQL) - Materialized views carbon-aware con refresh schedulato
-- PROBLEMA: Query aggregata pesante eseguita 1000x al giorno
-- Ogni esecuzione: 2-5 secondi, 100-500ms CPU, I/O intensivo
-- Stima: ~500mWh/giorno solo per questa query

-- Query pesante PRIMA (eseguita ad ogni richiesta)
SELECT
    c.category_id,
    c.name AS category_name,
    COUNT(DISTINCT o.order_id) AS total_orders,
    SUM(oi.quantity * oi.unit_price) AS total_revenue,
    AVG(oi.quantity * oi.unit_price) AS avg_order_value,
    COUNT(DISTINCT o.customer_id) AS unique_customers
FROM categories c
JOIN products p ON p.category_id = c.category_id
JOIN order_items oi ON oi.product_id = p.product_id
JOIN orders o ON o.order_id = oi.order_id
WHERE o.created_at >= NOW() - INTERVAL '30 days'
GROUP BY c.category_id, c.name;

-- SOLUZIONE: Materialized view con refresh in finestra verde
CREATE MATERIALIZED VIEW mv_category_metrics_30d AS
SELECT
    c.category_id,
    c.name AS category_name,
    COUNT(DISTINCT o.order_id) AS total_orders,
    SUM(oi.quantity * oi.unit_price) AS total_revenue,
    AVG(oi.quantity * oi.unit_price) AS avg_order_value,
    COUNT(DISTINCT o.customer_id) AS unique_customers,
    NOW() AS last_refreshed
FROM categories c
JOIN products p ON p.category_id = c.category_id
JOIN order_items oi ON oi.product_id = p.product_id
JOIN orders o ON o.order_id = oi.order_id
WHERE o.created_at >= NOW() - INTERVAL '30 days'
GROUP BY c.category_id, c.name
WITH DATA;

-- Indice per query O(1)
CREATE UNIQUE INDEX idx_mv_category_metrics ON mv_category_metrics_30d (category_id);

-- Refresh schedulato: CONCURRENT permette letture durante il refresh
-- Schedulare nelle finestre verdi (es. con pg_cron + carbon intensity check)
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_category_metrics_30d;

-- Con pg_cron (schedulare refresh ogni ora nelle ore diurne con solare)
SELECT cron.schedule(
    'refresh-category-metrics',
    '0 * * * *',  -- Ogni ora; logica carbon-aware nell'applicazione
    'REFRESH MATERIALIZED VIEW CONCURRENTLY mv_category_metrics_30d'
);

-- Query dopo: O(1) sul materialized view
-- Stima risparmio: 99% della computazione originale
SELECT * FROM mv_category_metrics_30d ORDER BY total_revenue DESC;


-- Pattern 2: Read Replica per separare carichi
-- Letture analitiche (intensive) -> read replica
-- Scritture -> primary (minimo carico)

-- Pattern 3: Partial Indexes per ridurre I/O
-- INVECE DI: indice su tutti i 50M ordini
CREATE INDEX idx_orders_status_all ON orders(status, created_at);

-- MEGLIO: indice solo sui 500K ordini attivi (1% del totale)
-- 99% meno I/O, 99% meno spazio, maintenance molto più veloce
CREATE INDEX idx_orders_status_active ON orders(status, created_at)
WHERE status IN ('pending', 'processing', 'shipped');

-- Pattern 4: Connection Pooling per ridurre overhead
-- PgBouncer: max_client_conn=1000, pool_size=20
-- Riduce: TCP handshakes, SSL negotiation, process fork overhead
-- Stima risparmio: 30-50% CPU PostgreSQL su workload high-concurrency

Query Optimization: N+1 Problem e Eager Loading

Il problema N+1 è uno dei più comuni e costosi anti-pattern in termini di emissioni: invece di una singola query che recupera tutti i dati necessari, si eseguono N+1 query separate. Con N=1000 ordini, si generano 1001 query invece di 1, moltiplicando per 1000 il carbon footprint dell'operazione.

Python (SQLAlchemy) - Eliminare N+1 con eager loading
from sqlalchemy import select
from sqlalchemy.orm import selectinload, joinedload, Session
from typing import Sequence

# ANTI-PATTERN: N+1 queries - EVITARE
def get_orders_naive(session: Session, limit: int = 100) -> list:
    """
    PROBLEMATICO: per 100 ordini genera 101 query.
    100 ordini -> 1 query
    100 customer -> 100 query separate
    Stima: ~2mWh per 100 ordini. Su 10.000 richieste/giorno = 20Wh/giorno.
    """
    orders = session.execute(select(Order).limit(limit)).scalars().all()
    # Ogni accesso a order.customer triggersa una nuova query! (lazy loading)
    return [{"id": o.id, "customer": o.customer.email} for o in orders]

# PATTERN GREEN: Eager loading con selectin
def get_orders_green(session: Session, limit: int = 100) -> list:
    """
    OTTIMIZZATO: 2 query totali invece di N+1.
    Query 1: tutti gli ordini
    Query 2: tutti i customer in una sola query IN
    Stima: 0.02mWh per 100 ordini. Risparmio: 99%.
    """
    stmt = (
        select(Order)
        .options(selectinload(Order.customer))  # 2 query totali
        .limit(limit)
    )
    orders = session.execute(stmt).scalars().all()
    return [{"id": o.id, "customer": o.customer.email} for o in orders]

# Ancora meglio: joinedload per 1 sola query
def get_orders_single_query(session: Session, limit: int = 100) -> list:
    """
    ULTRA-OTTIMIZZATO: 1 sola query con JOIN.
    Ideale quando il numero di relazioni e basso.
    Stima: 0.01mWh per 100 ordini. Risparmio: 99.5%.
    """
    stmt = (
        select(Order)
        .options(joinedload(Order.customer))  # JOIN: 1 sola query
        .limit(limit)
    )
    orders = session.execute(stmt).unique().scalars().all()
    return [{"id": o.id, "customer": o.customer.email} for o in orders]


# Pattern: Projection - seleziona solo i campi necessari
def get_order_summary(session: Session, order_ids: list[int]) -> list[dict]:
    """
    Seleziona SOLO i campi necessari, non SELECT *.
    Su tabelle con 50+ colonne, SELECT * trasferisce 10-20x più dati.
    """
    stmt = (
        select(
            Order.id,
            Order.total_amount,
            Order.status,
            # Solo 3 campi invece di 50+
        )
        .where(Order.id.in_(order_ids))
    )
    rows = session.execute(stmt).all()
    return [{"id": r.id, "total": float(r.total_amount), "status": r.status} for r in rows]

Network Efficiency: Ogni Byte Trasferito Ha un Costo Carbon

Trasferire dati ha un costo energetico reale: 0.06-0.1 kWh per GB per il traffico internet (backbone + last mile). Un'applicazione che trasferisce 10TB di dati non compressi al giorno consuma circa 600-1000 kWh solo per la trasmissione, equivalente a 200-400 kg CO₂ al giorno (con intensità media europea di 350 gCO₂/kWh).

HTTP/3 e QUIC: Efficienza a Livello di Protocollo

HTTP/3 con QUIC elimina il problema del head-of-line blocking e riduce i round-trip necessari per stabilire connessioni. Per applicazioni con molte richieste concorrenti, HTTP/3 può ridurre la latenza del 15-30% e, di conseguenza, il tempo di CPU attivo dei server.

TypeScript/Node.js - Compressione HTTP e ottimizzazione response size
import express, { Request, Response, NextFunction } from "express";
import compression from "compression";
import { createBrotliCompress, createGzip } from "zlib";
import { pipeline } from "stream/promises";

const app = express();

// Compressione intelligente: scegli il miglior algoritmo
// Brotli: migliore ratio (15-25% superiore a gzip), supportato da tutti i browser moderni
// Gzip: fallback per client vecchi
app.use(compression({
  // Comprimi solo se il risparmio e significativo
  threshold: 1024, // Min 1KB per comprimere
  level: 6,        // Bilanciamento CPU/ratio
  filter: (req, res) => {
    // Non comprimere immagini già compresse (jpeg, webp, png)
    const contentType = res.getHeader("Content-Type") as string || "";
    if (contentType.includes("image/")) return false;
    return compression.filter(req, res);
  },
}));

// Middleware: risposta minima con field selection
// Invece di returnare tutto l'oggetto, risponde solo con i campi richiesti
function fieldSelectionMiddleware(req: Request, res: Response, next: NextFunction): void {
  const originalJson = res.json.bind(res);

  res.json = (body: any) => {
    const fields = req.query["fields"];
    if (!fields || typeof fields !== "string" || !body) {
      return originalJson(body);
    }

    const requestedFields = fields.split(",").map(f => f.trim());

    // Filtra solo i campi richiesti (non muta body originale)
    const filtered = Array.isArray(body)
      ? body.map(item => pickFields(item, requestedFields))
      : pickFields(body, requestedFields);

    return originalJson(filtered);
  };

  next();
}

function pickFields(obj: Record<string, unknown>, fields: string[]): Record<string, unknown> {
  return fields.reduce<Record<string, unknown>>((acc, field) => {
    if (field in obj) {
      return { ...acc, [field]: obj[field] };
    }
    return acc;
  }, {});
}

app.use(fieldSelectionMiddleware);

// Esempio: GET /api/users?fields=id,name,email
// Invece di returnare 50 campi, restituisce solo 3
// Riduzione payload tipica: 60-90%

// Cache-Control headers: riduce richieste ripetute
function addCacheHeaders(res: Response, maxAgeSeconds: number): void {
  res.setHeader("Cache-Control", `public, max-age={maxAgeSeconds}, stale-while-revalidate=60`);
  res.setHeader("Vary", "Accept-Encoding, Accept");
}

app.get("/api/products/:id", async (req: Request, res: Response) => {
  const product = await getProduct(req.params["id"]);

  // Dati statici: cache aggressiva
  if (product?.isStatic) {
    addCacheHeaders(res, 86400); // 1 giorno
  } else {
    addCacheHeaders(res, 300); // 5 minuti
  }

  res.json(product);
});

// ETag per validazione cache efficiente
// Invece di re-scaricare, il client verifica se il dato e cambiato
app.get("/api/catalog", async (req: Request, res: Response) => {
  const catalog = await getCatalog();
  const etag = require("crypto")
    .createHash("md5")
    .update(JSON.stringify(catalog))
    .digest("hex");

  // Se ETag non cambiato: risponde 304 (0 bytes di payload!)
  if (req.headers["if-none-match"] === etag) {
    res.status(304).end();
    return;
  }

  res.setHeader("ETag", etag);
  res.json(catalog);
});

async function getProduct(id: string): Promise<any> {
  return { id, isStatic: true, name: "Product" };
}
async function getCatalog(): Promise<any[]> {
  return [];
}

Frontend Pattern Sostenibili: Il Client È Parte del Problema

Il frontend è spesso il componente più trascurato nell'analisi delle emissioni software, ma eseguire JavaScript pesante su milioni di dispositivi ha un impatto aggregato enorme. Uno script JS da 500KB eseguito su 1 milione di dispositivi richiede circa 50 GWh di energia annuale sui device degli utenti - non nei nostri server.

Image Optimization: Il Guadagno Più Veloce

Le immagini rappresentano in media il 50-70% del peso di una pagina web. Passare da JPEG a WebP/AVIF riduce le dimensioni del 25-50%, con equivalente riduzione nel trasferimento dati e nel tempo di decodifica sul device dell'utente.

TypeScript (Angular) - Immagini sostenibili con lazy loading e modern formats
// angular.json - abilita image optimization built-in di Angular
// Angular 17+ ha NgOptimizedImage integrato

import { NgOptimizedImage } from '@angular/common';
import { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-product-card',
  standalone: true,
  imports: [NgOptimizedImage],
  changeDetection: ChangeDetectionStrategy.OnPush, // Riduce change detection cycles
  template: `
    <!-- NgOptimizedImage: lazy loading + srcset automatico + WebP -->
    <img
      ngSrc="products/laptop-pro.jpg"
      [width]="400"
      [height]="300"
      [priority]="isAboveFold"
      loading="lazy"
      decoding="async"
      alt="Laptop Pro - Vista frontale"
    />
  `
})
export class ProductCardComponent {
  isAboveFold = false;
}

// build pipeline: conversione automatica a WebP/AVIF con sharp
// scripts/optimize-images.mjs
import sharp from 'sharp';
import { readdir, stat } from 'fs/promises';
import path from 'path';

async function optimizeImages(inputDir: string, outputDir: string): Promise<void> {
  const files = await readdir(inputDir);
  const imageFiles = files.filter(f => /\.(jpg|jpeg|png)$/i.test(f));

  const results = await Promise.all(imageFiles.map(async (file) => {
    const inputPath = path.join(inputDir, file);
    const baseName = path.basename(file, path.extname(file));

    // Genera WebP (supporto universale 2025)
    const webpPath = path.join(outputDir, `{baseName}.webp`);
    await sharp(inputPath)
      .webp({ quality: 80, effort: 6 })
      .toFile(webpPath);

    // Genera AVIF (compressione superiore, browser moderni)
    const avifPath = path.join(outputDir, `{baseName}.avif`);
    await sharp(inputPath)
      .avif({ quality: 65, effort: 7 })
      .toFile(avifPath);

    const [origSize, webpSize, avifSize] = await Promise.all([
      stat(inputPath).then(s => s.size),
      stat(webpPath).then(s => s.size),
      stat(avifPath).then(s => s.size),
    ]);

    return {
      file,
      origKB: (origSize / 1024).toFixed(1),
      webpKB: (webpSize / 1024).toFixed(1),
      avifKB: (avifSize / 1024).toFixed(1),
      webpSaving: ((1 - webpSize/origSize) * 100).toFixed(1) + '%',
      avifSaving: ((1 - avifSize/origSize) * 100).toFixed(1) + '%',
    };
  }));

  console.table(results);
}

Dark Mode e Risparmio Energetico su OLED

Dark Mode: Impatto Reale sui Display OLED

Sui display OLED (presenti in tutti gli smartphone premium e molti laptop dal 2024), i pixel neri consumano letteralmente 0 energia (il pixel OLED non emette luce se spento). Un'interfaccia con sfondo nero puro (#000000) su OLED può consumare fino al 60-80% meno energia del display rispetto a sfondo bianco (#FFFFFF). Con miliardi di dispositivi OLED, offrire un dark mode ben implementato è una riduzione significativa delle emissioni lato utente.

CSS - Dark mode ottimizzato per OLED con true black
/* Dark mode: usa true black (#000000) per massimizzare risparmio OLED */
/* Anche #0d0d0d e sufficiente e più gradevole esteticamente */

:root {
  --bg-primary: #ffffff;
  --bg-secondary: #f5f5f5;
  --text-primary: #1a1a1a;
  --surface: #ffffff;
}

/* Auto dark mode: si attiva in base alla preferenza di sistema */
@media (prefers-color-scheme: dark) {
  :root {
    --bg-primary: #000000;      /* True black per OLED */
    --bg-secondary: #0d0d0d;    /* Quasi-nero, più comfort visivo */
    --text-primary: #e8e8e8;
    --surface: #111111;
  }
}

/* Classe manuale per toggle */
.dark-theme {
  --bg-primary: #000000;
  --bg-secondary: #0d0d0d;
  --text-primary: #e8e8e8;
  --surface: #111111;
}

/* Ridurre motion: riduce animazioni = meno GPU = meno energia */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Font system: evita Google Fonts quando possibile */
/* Font di sistema = 0 download, rendering istantaneo */
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}

/* Se font custom e necessario: usa font-display: swap per evitare blocking */
@font-face {
  font-family: "CustomFont";
  src: url("/fonts/custom.woff2") format("woff2");
  font-display: swap;  /* Non blocca rendering mentre scarica */
  font-weight: 400 700;  /* Variable font: 1 file per tutti i pesi */
}

API Design Sostenibile: Meno Round-Trip, Meno Carbon

La progettazione delle API ha un impatto diretto sulle emissioni: API mal progettate richiedono più round-trip, trasferiscono più dati del necessario, e costringono i client a fare più richieste per ottenere le informazioni di cui hanno bisogno.

Pagination Efficiente: Cursor vs Offset

La pagination con OFFSET è energeticamente costosa: per recuperare la pagina 100 con 20 elementi, il database deve scorrere e scartare 2000 righe (OFFSET 2000). Con cursor-based pagination, il database salta direttamente al punto corretto usando un indice, consumando energia proporzionale solo al numero di righe restituite, non a quelle saltate.

TypeScript - Cursor pagination vs offset pagination - impatto energetico
import { Pool } from "pg";

interface PaginationResult<T> {
  readonly data: readonly T[];
  readonly nextCursor: string | null;
  readonly totalCount?: number;
}

// ANTI-PATTERN: Offset pagination
// Costo query: O(offset + limit) - cresce con la profondità di paginazione
async function getProductsOffset(
  db: Pool,
  page: number,
  pageSize: number = 20
): Promise<PaginationResult<any>> {
  const offset = page * pageSize;
  // COSTOSO: il DB scorre 'offset' righe per poi scartarle
  const result = await db.query(
    "SELECT * FROM products ORDER BY id LIMIT $1 OFFSET $2",
    [pageSize, offset]
  );
  // Esempio pagina 1000 con 20 items: DB scorre 20.020 righe
  // Stima: 50x più lenta e 50x più dispendiosa di cursor pagination
  return { data: result.rows, nextCursor: null };
}

// PATTERN GREEN: Cursor-based pagination
// Costo query: O(limit) - costante, usa indice
async function getProductsCursor(
  db: Pool,
  cursor: string | null,
  pageSize: number = 20
): Promise<PaginationResult<any>> {
  let query: string;
  let params: any[];

  if (cursor) {
    // Decode cursor: contiene l'ID dell'ultimo elemento visto
    const lastId = parseInt(Buffer.from(cursor, "base64").toString("utf-8"));
    query = `
      SELECT id, name, price, category_id
      FROM products
      WHERE id > $1
      ORDER BY id ASC
      LIMIT $2
    `;
    params = [lastId, pageSize + 1]; // +1 per sapere se c'è una pagina successiva
  } else {
    query = `
      SELECT id, name, price, category_id
      FROM products
      ORDER BY id ASC
      LIMIT $1
    `;
    params = [pageSize + 1];
  }

  const result = await db.query(query, params);
  const rows = result.rows;

  const hasMore = rows.length > pageSize;
  const data = hasMore ? rows.slice(0, pageSize) : rows;
  const lastItem = data[data.length - 1];

  // Encode next cursor
  const nextCursor = hasMore && lastItem
    ? Buffer.from(String(lastItem.id)).toString("base64")
    : null;

  return {
    data,
    nextCursor,
    // Non calcola totalCount (costoso): usa solo se necessario con estimate
  };
}


// GraphQL vs REST: quando ognuno e più efficiente
// REST: efficiente per risorse semplici e ben definite, cache HTTP nativa
// GraphQL: efficiente per UI complesse con molti componenti che richiedono dati diversi

// Pattern GraphQL carbon-aware: DataLoader per batch N+1
import DataLoader from "dataloader";
import { GraphQLResolveInfo } from "graphql";

// Senza DataLoader: 1000 resolver -> 1000 query SQL separate
// Con DataLoader: 1000 resolver -> 1 query SQL con WHERE IN
const productLoader = new DataLoader<number, any>(
  async (productIds: readonly number[]) => {
    const result = await db.query(
      "SELECT * FROM products WHERE id = ANY($1)",
      [[...productIds]]
    );
    // Mappa per mantenere l'ordine corretto
    const productMap = new Map(result.rows.map(p => [p.id, p]));
    return productIds.map(id => productMap.get(id) || null);
  },
  { batch: true, maxBatchSize: 100 } // Max 100 per query per sicurezza
);

declare const db: Pool;

Carbon Monitoring: Misurare per Migliorare

"Ciò che non si misura non si può migliorare." Il carbon monitoring a livello di servizio permette di identificare i componenti più emissivi, tracciare i progressi nel tempo, e calcolare il SCI score (Software Carbon Intensity) come KPI aziendale.

Python - Carbon metrics con Prometheus per servizio
from prometheus_client import Counter, Histogram, Gauge, start_http_server
from functools import wraps
import time
import httpx
import logging
from typing import Callable, Any

logger = logging.getLogger(__name__)

# Metriche Prometheus custom per carbon monitoring
CARBON_INTENSITY_GAUGE = Gauge(
    "carbon_intensity_g_co2_per_kwh",
    "Current grid carbon intensity in gCO2/kWh",
    ["region"]
)

ENERGY_CONSUMED_COUNTER = Counter(
    "energy_consumed_wh_total",
    "Total energy consumed in Wh",
    ["service", "endpoint", "method"]
)

CARBON_EMITTED_COUNTER = Counter(
    "carbon_emitted_gco2_total",
    "Total carbon emitted in gCO2",
    ["service", "endpoint", "method"]
)

REQUEST_DURATION_HISTOGRAM = Histogram(
    "http_request_duration_seconds",
    "HTTP request duration",
    ["service", "endpoint", "method"],
    buckets=[0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5]
)

SCI_SCORE_GAUGE = Gauge(
    "sci_score_mgco2_per_request",
    "Software Carbon Intensity score in mgCO2 per functional unit",
    ["service"]
)

# Stima consumo energetico per tipo di risorsa
# Basato su benchmark empirici per server tipici (TDP ~200W, utilization 20%)
ENERGY_ESTIMATES_WH = {
    "cpu_second": 0.011,           # ~11 mWh per secondo di CPU @ 200W TDP, 20% util
    "memory_gb_second": 0.000375,  # ~0.375 mWh per GB*s RAM
    "ssd_read_gb": 0.0002,         # ~0.2 mWh per GB letto da SSD
    "ssd_write_gb": 0.0004,        # ~0.4 mWh per GB scritto su SSD
    "network_gb": 0.1,             # ~100 mWh per GB trasferito (rete)
}

class CarbonMetricsCollector:
    """Raccoglie metriche carbon per Prometheus + Grafana."""

    def __init__(self, service_name: str, region: str = "IT"):
        self._service = service_name
        self._region = region
        self._current_intensity = 350.0  # Default gCO2/kWh

    async def update_carbon_intensity(self) -> None:
        """Aggiorna l'intensità carbonica dalla grid in tempo reale."""
        try:
            async with httpx.AsyncClient(timeout=5) as client:
                resp = await client.get(
                    "https://api.electricitymap.org/v3/carbon-intensity/latest",
                    params={"zone": self._region},
                    headers={"auth-token": "YOUR_TOKEN"}
                )
                data = resp.json()
                self._current_intensity = data["carbonIntensity"]
                CARBON_INTENSITY_GAUGE.labels(region=self._region).set(self._current_intensity)
        except Exception as e:
            logger.warning("Carbon intensity fetch failed: %s", e)

    def track_request(self, endpoint: str, method: str):
        """Decorator per tracciare carbon footprint di ogni endpoint."""
        def decorator(func: Callable) -> Callable:
            @wraps(func)
            async def wrapper(*args: Any, **kwargs: Any) -> Any:
                start = time.perf_counter()
                result = await func(*args, **kwargs)
                duration_s = time.perf_counter() - start

                # Stima energia: basata sulla durata (proxy per CPU+I/O)
                estimated_energy_wh = duration_s * ENERGY_ESTIMATES_WH["cpu_second"]
                # Carbon emessa
                carbon_g = estimated_energy_wh * self._current_intensity / 1000

                # Aggiorna metriche Prometheus
                labels = {"service": self._service, "endpoint": endpoint, "method": method}
                ENERGY_CONSUMED_COUNTER.labels(**labels).inc(estimated_energy_wh * 1000)  # in mWh
                CARBON_EMITTED_COUNTER.labels(**labels).inc(carbon_g * 1000)  # in mgCO2
                REQUEST_DURATION_HISTOGRAM.labels(**labels).observe(duration_s)

                return result
            return wrapper
        return decorator

    def update_sci_score(self, total_carbon_mgco2: float, functional_units: int) -> None:
        """
        Aggiorna il SCI score: mgCO2 per functional unit (es. per request).
        Formula SCI: (E * I + M) / R
        E = energia, I = intensità, M = embodied carbon, R = functional units
        """
        if functional_units > 0:
            sci = total_carbon_mgco2 / functional_units
            SCI_SCORE_GAUGE.labels(service=self._service).set(sci)


# Grafana dashboard query examples (PromQL):
#
# Carbon footprint per endpoint (top 10 più emissivi):
# topk(10, sum by (endpoint) (rate(carbon_emitted_gco2_total[5m])))
#
# SCI score nel tempo:
# sci_score_mgco2_per_request{service="api-gateway"}
#
# Risparmio carbon da cache hits:
# rate(cache_hits_total[5m]) * 0.005  # 5mg CO2 per hit risparmiato
#
# Carbon intensity corrente vs soglia verde:
# carbon_intensity_g_co2_per_kwh > 300  # Alert se supera soglia

Caso Studio: E-Commerce da 1M Visite/Giorno - -45% Carbon Footprint

Vediamo come tutti questi pattern si integrano in un caso concreto. Un e-commerce con 1 milione di visite giornaliere, 50.000 ordini/giorno, e un'architettura legacy non ottimizzata. Il team ha implementato i pattern descritti in questo articolo in un progetto di 6 mesi. I risultati parlano da soli.

Baseline dell'Architettura Prima dell'Ottimizzazione

Componente Configurazione Prima Problema Carbon/mese (stima)
Web servers 20x c5.2xlarge always-on CPU media 8%, 92% idle 450 kg CO₂
Database db.r5.4xlarge, SELECT * ovunque N+1 queries, nessun indice 280 kg CO₂
Storage S3 Tutto in Standard tier 5 anni di log mai acceduti in hot storage 90 kg CO₂
Batch jobs Fisso alle 02:00 UTC Nessuna carbon awareness 120 kg CO₂
CDN/Cache Cache hit rate 35% TTL troppo corti, nessun edge caching 180 kg CO₂
Frontend Bundle JS 2.8MB, JPEG Nessuna code splitting, immagini non ottimizzate 200 kg CO₂
Totale 1.320 kg CO₂/mese

Risultati Dopo Ottimizzazione (6 Mesi)

Intervento Pattern Applicato Riduzione Carbon Tempo Implementazione
Auto-scaling + right-sizing EC2 Scale down aggressivo, spot instances batch -195 kg CO₂/mese (43%) 2 settimane
Query optimization + materialized views N+1 fix, projection, partial indexes -140 kg CO₂/mese (50%) 4 settimane
S3 lifecycle policies Tiered storage: hot/warm/cold/archive -72 kg CO₂/mese (80%) 1 settimana
Carbon-aware batch scheduling Carbon Aware SDK, finestre verdi -48 kg CO₂/mese (40%) 3 settimane
Multi-level cache (Redis + CDN) Cache hit rate: 35% -> 87% -126 kg CO₂/mese (70%) 6 settimane
Frontend: WebP/AVIF + code splitting Bundle 2.8MB -> 380KB, WebP images -110 kg CO₂/mese (55%) 3 settimane
Totale risparmiato -691 kg CO₂/mese (-52%) 6 mesi totali

Il risultato finale è una riduzione del 52% del carbon footprint mensile (da 1.320 kg a 629 kg CO₂), con un risparmio annuale di circa 8.3 tonnellate di CO₂ equivalente. Equivalente a eliminare circa 4 auto dalle strade per un anno. E non è stato necessario riscrivere l'applicazione da zero: tutti gli interventi sono stati incrementali.

Lezione Chiave dal Caso Studio

I due interventi con il miglior ROI (carbon saved / effort) sono stati:

  • S3 lifecycle policies (80% riduzione, 1 settimana di lavoro): bastano poche righe di configurazione Infrastructure-as-Code. È il pattern con il rapporto impatto/sforzo più alto in assoluto.
  • Auto-scaling e right-sizing (43% riduzione, 2 settimane): la stragrande maggioranza dei sistemi è over-provisioned. Ridurre il provisioning al minimo necessario con auto-scaling ha un impatto immediato e misurabile.

Morale: inizia sempre dai pattern più semplici. L'80% dei guadagni viene dal 20% degli interventi.

Anti-Pattern da Evitare: Le 5 Trappole Più Comuni

Anti-Pattern #1: Cache Warming Aggressivo Fuori Orario Verde

Pre-caricare la cache in modo massiccio durante picchi di carbon intensity (tipicamente sera, quando il solare è assente e il gas copre la domanda) consuma più energia di quanta ne risparmia. Il cache warming va schedulato nelle finestre verdi identificate dal Carbon Aware SDK.

Anti-Pattern #2: Log Verbosi in Hot Storage

Conservare anni di log di debug in S3 Standard (hot tier) è uno spreco economico ed energetico. I log di più di 30 giorni raramente vengono acceduti; dopo 90 giorni, quasi mai. Implementare lifecycle policies è la prima azione da fare in qualsiasi sistema legacy.

Anti-Pattern #3: Always-On per Workload Intermittenti

Un servizio che riceve 10 richieste al giorno non ha bisogno di girare 24/7 su un'istanza dedicata. Lambda, Cloud Run o Fargate con scale-to-zero è la scelta green per workload intermittenti. Un'istanza EC2 t3.small always-on emette ~15 kg CO₂/mese; Lambda per 10 richieste/giorno emette <0.001 kg CO₂/mese. 15.000 volte meno.

Anti-Pattern #4: SELECT * in Produzione

SELECT * trasferisce tutti i campi di una tabella anche quando l'applicazione ne usa solo 2-3. Su tabelle con 50+ colonne e milioni di righe, questo moltiplica per 10-20 i dati trasferiti dal database, con equivalente impatto energetico. Usare sempre la projection esplicita.

Anti-Pattern #5: Bundle JavaScript Monolitico

Un bundle JS da 3MB scaricato e parsato da 1 milione di utenti al giorno richiede circa 2.000 kWh di energia CPU sui device degli utenti. Con code splitting e lazy loading, il bundle iniziale può scendere a 100-200KB, riducendo di 15-30x il consumo energetico lato client. L'energia consumata sui dispositivi degli utenti è parte dello Scope 3 del software.

Checklist: Pattern Sostenibili per il Tuo Prossimo Sprint

Priorità Alta (Impatto Massimo, Bassa Complessità)

  • Implementa S3/GCS lifecycle policies per spostare dati verso tier freddi automaticamente
  • Analizza e riduci il provisioning delle istanze EC2/GCE (AWS Compute Optimizer, GCP Recommender)
  • Aggiungi schedule shutdown per ambienti development/staging fuori orario lavorativo
  • Abilita compressione gzip/brotli su tutti gli endpoint HTTP (se non già attiva)
  • Converti le immagini in WebP/AVIF nel pipeline di build
  • Rimuovi tutti i SELECT * sostituendoli con projection esplicita

Priorità Media (Alto Impatto, Complessità Moderata)

  • Implementa multi-level caching (L1 in-process + L2 Redis) per le risorse più accedute
  • Crea materialized views per le query aggregate eseguite frequentemente
  • Migra i batch job time-flexible a Carbon Aware SDK scheduling
  • Risolvi i problemi N+1 con eager loading o DataLoader (GraphQL)
  • Implementa cursor-based pagination in sostituzione di offset pagination
  • Aggiungi ETag e Cache-Control appropriati alle risposte API

Priorità Bassa (Miglioramenti Incrementali)

  • Valuta migrazione a HTTP/3 per applicazioni ad alta latenza sensibilità
  • Implementa field selection nei REST endpoint (query param ?fields=)
  • Aggiungi dark mode con true black per ridurre consumo su dispositivi OLED degli utenti
  • Configura carbon monitoring con Prometheus + dashboard Grafana per SCI score
  • Introduci partial indexes per query filtrate su subset piccoli di dati
  • Valuta serverless (Lambda, Cloud Functions) per workload con <100 richieste/ora

Conclusioni: Architettura Sostenibile Come Pratica Standard

I pattern architetturali sostenibili non sono un lusso riservato alle grandi tech company con budget per la sostenibilità: sono buone pratiche ingegneristiche che migliorano contemporaneamente performance, costi e impatto ambientale. Tiered storage, caching efficiente, batch scheduling carbon-aware, right-sizing e query optimization producono sistemi più efficienti sotto ogni dimensione.

Il caso studio mostra che è possibile ridurre il carbon footprint di un sistema legacy del 50%+ in 6 mesi con interventi incrementali, senza dover riscrivere l'architettura da zero. La chiave è iniziare dagli interventi ad alto impatto e bassa complessità (lifecycle policies, right-sizing, query optimization), misurare i risultati con metriche SCI, e poi procedere verso ottimizzazioni più sofisticate.

La Direttiva CSRD (Corporate Sustainability Reporting Directive), in vigore per le grandi imprese europee dal 2025 e per le medie imprese dal 2026, richiederà a molte organizzazioni di rendicontare le emissioni software come parte delle emissioni Scope 3. Le architetture sostenibili non sono più solo una scelta etica: diventano un requisito normativo e competitivo.

Prossimo Articolo della Serie

Il decimo e ultimo articolo completerà la serie Green Software con ESG, CSRD e Compliance per Software Team: come strutturare il reporting obbligatorio delle emissioni software, creare audit trail per i regolatori, e integrare le metriche SCI nei processi di governance aziendale. Tratteremo anche gli strumenti pratici per il reporting: GHG Protocol, ESRS E1, e il framework Impact di GSF.

Risorse Aggiuntive

  • Carbon Aware SDK (Green Software Foundation): github.com/Green-Software-Foundation/carbon-aware-sdk
  • Electricity Maps API: Dati real-time intensità carbonica per 50+ regioni
  • AWS Compute Optimizer: Raccomandazioni automatiche di right-sizing
  • Impact Framework (GSF): Calcolo SCI per componente
  • Web Almanac 2024 (HTTP Archive): Statistiche reali su performance e size web
  • Cloud Carbon Footprint: Tool open source per misurare emissioni cloud multi-provider

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti