Skip to content
SP StackPractices
intermediate

Caching y Memoización

Cómo cachear computaciones costosas y respuestas de API usando caches en memoria, LRU y distribuidos en Python, JavaScript y Java.

Temas: data

Visión general

El caching almacena el resultado de computaciones costosas para que requests posteriores por los mismos datos puedan ser servidos más rápido. La memoización es una forma específica de caching donde los valores de retorno de funciones se cachean basados en sus argumentos.

El caching es una de las optimizaciones de rendimiento más efectivas, pero introduce complejidad: datos stale, invalidación de cache y consistencia distribuida.

Cuándo usarlo

Usa esta recipe cuando:

  • Llamas queries de base de datos o endpoints de API costosos repetidamente
  • Computas resultados matemáticos o estadísticos complejos
  • Sirves datos de configuración estáticos o de cambio lento
  • Reduces latencia en sistemas de lectura intensa de alto tráfico
  • Descargas carga de servicios downstream

Solución

Python

from functools import lru_cache
from cachetools import TTLCache

# Memoización LRU built-in
@lru_cache(maxsize=128)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(100))  # Instantáneo, cacheado

# TTL cache con expiración
api_cache = TTLCache(maxsize=100, ttl=300)  # 5 minutos

def fetch_user(user_id):
    if user_id in api_cache:
        return api_cache[user_id]
    user = db.query("SELECT * FROM users WHERE id = %s", user_id)
    api_cache[user_id] = user
    return user

JavaScript

// Memoización simple
function memoize(fn) {
  const cache = new Map();
  return (...args) => {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

const fib = memoize((n) => (n < 2 ? n : fib(n - 1) + fib(n - 2)));
console.log(fib(100)); // Instantáneo

// LRU cache con límite de tamaño
class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.cache = new Map();
  }
  get(key) {
    if (!this.cache.has(key)) return undefined;
    const value = this.cache.get(key);
    this.cache.delete(key);
    this.cache.set(key, value); // Mover al final (más reciente)
    return value;
  }
  set(key, value) {
    if (this.cache.has(key)) this.cache.delete(key);
    else if (this.cache.size >= this.capacity) {
      const first = this.cache.keys().next().value;
      this.cache.delete(first);
    }
    this.cache.set(key, value);
  }
}

Java

import com.github.benmanes.caffeine.cache.*;

Cache<String, User> userCache = Caffeine.newBuilder()
    .maximumSize(100)
    .expireAfterWrite(Duration.ofMinutes(5))
    .build();

// Get or compute
User user = userCache.get(userId, id -> db.findById(id));

// Put manual
userCache.put(userId, updatedUser);

// Invalidar
userCache.invalidate(userId);

Estrategias de Invalidación de Cache

EstrategiaCuándo usarCompromiso
TTL (Time To Live)Los datos cambian predeciblementePuede servir datos stale brevemente
Write-throughLa consistencia es críticaWrites más lentos, reads más simples
Write-behindAlto throughput de escrituraRiesgo de pérdida de datos en crash
Cache-asideFlexibilidad, lectura intensivaLa aplicación maneja la lógica de cache
Eviction (LRU/LFU)Restricciones de memoriaPuede evictar datos hot prematuramente

Mejores prácticas

  • Cachea al nivel correcto: No cachees todo. Cachea los datos más costosos y más frecuentemente accedidos.
  • Establece TTLs consideradamente: Demasiado corto = inútil. Demasiado largo = datos stale.
  • Monitorea hit rates: Un cache con <80% hit rate generalmente no vale la complejidad.
  • Maneja fallos de cache graceful: Si Redis está caído, fallback a la base de datos. No falles el request.
  • Versiona cache keys: Incluye la versión de datos o app en la key para prevenir datos stale después de deploys.
  • Invalida proactivamente: Limpia entradas de cache cuando los datos subyacentes cambian, no solo cuando expira el TTL.

Errores comunes

  • Cachear datos que cambian demasiado frecuentemente o raramente se solicitan
  • No manejar cache stampede (thundering herd) cuando expira el TTL
  • Almacenar caches sin bounds que crecen hasta out-of-memory
  • Ignorar consistencia de cache en sistemas distribuidos
  • Olvidar invalidar cache después de mutaciones

Preguntas frecuentes

P: ¿Qué es cache stampede y cómo lo prevengo? R: El cache stampede ocurre cuando muchos requests golpean simultáneamente una key de cache faltante. Usa locking, semáforos per-key, o expiración temprana probabilística.

P: ¿Cuándo debería usar Redis en lugar de caching en memoria? R: Usa Redis cuando necesites cache compartido entre múltiples instancias de aplicación, persistencia, o estructuras de datos avanzadas.

P: ¿Debería cachear respuestas de API? R: Sí, si los datos son cacheables y el endpoint es de lectura intensa. Usa el header Cache-Control para comunicar cacheability a clientes y CDNs.