Patrón Cache-Aside
Carga datos en el caché bajo demanda desde el almacenamiento principal. Un patrón de caché que da a la aplicación control total sobre qué y cuándo cachear.
Resumen
Cada lectura repetida contra la base de datos cuesta latencia y capacidad de conexión en cada petición. El Patrón Cache-Aside pone a la aplicación a cargo de resolverlo: verifica el caché primero y, ante un miss, obtiene el dato de la base de datos, lo guarda en el caché y lo devuelve. La aplicación es dueña de la lógica de caché, la invalidación y la consistencia — no se delega al proveedor de caché.
Encaja en sistemas donde dominan las lecturas y unos segundos de datos obsoletos son aceptables. En cargas de trabajo con muchas escrituras, o cuando cada lectura exige consistencia fuerte, el caché write-through es mejor opción.
Cuándo usarlo
Usa el Patrón Cache-Aside cuando:
- Cargas de trabajo de lectura intensiva donde los mismos datos se solicitan con frecuencia
- La aplicación deba controlar qué se cachea y por cuánto tiempo
- La invalidación de caché pueda manejarse explícitamente desde la capa de aplicación
- Quieras una estrategia de caché simple y portable que funcione con cualquier proveedor (Redis, Memcached, en memoria)
- Ejemplos: perfiles de usuario, catálogos de productos, datos de configuración, datos de referencia
Evítalo cuando dominan las escrituras — el caché rota y casi nunca acierta — o cuando una aplicación pequeña no necesita una capa de caché en absoluto.
Solución
Python
import time
from typing import Any, Callable
class CacheAside:
def __init__(self, cache: dict, ttl_seconds: float = 60):
self.cache = cache
self.ttl = ttl_seconds
self.timestamps = {}
def get(self, key: str, loader: Callable[[], Any]) -> Any:
now = time.time()
if key in self.cache:
if now - self.timestamps.get(key, 0) < self.ttl:
print(f"Cache hit: {key}")
return self.cache[key]
else:
del self.cache[key]
print(f"Cache miss: {key}")
value = loader()
self.cache[key] = value
self.timestamps[key] = now
return value
def invalidate(self, key: str):
self.cache.pop(key, None)
self.timestamps.pop(key, None)
# Uso
cache = {}
store = CacheAside(cache)
def load_user(user_id: int) -> dict:
# Simula llamada a DB
return {"id": user_id, "name": f"User {user_id}"}
user = store.get("user:1", lambda: load_user(1))
user = store.get("user:1", lambda: load_user(1)) # Cache hit
store.invalidate("user:1")
JavaScript
class CacheAside {
constructor(cache, ttlMs = 60000) {
this.cache = cache;
this.ttl = ttlMs;
this.timestamps = new Map();
}
get(key, loader) {
const now = Date.now();
if (this.cache.has(key)) {
if (now - (this.timestamps.get(key) || 0) < this.ttl) {
console.log(`Cache hit: ${key}`);
return this.cache.get(key);
}
this.cache.delete(key);
}
console.log(`Cache miss: ${key}`);
const value = loader();
this.cache.set(key, value);
this.timestamps.set(key, now);
return value;
}
invalidate(key) {
this.cache.delete(key);
this.timestamps.delete(key);
}
}
// Uso
const cache = new Map();
const store = new CacheAside(cache);
function loadUser(userId) {
return { id: userId, name: `User ${userId}` };
}
let user = store.get("user:1", () => loadUser(1));
user = store.get("user:1", () => loadUser(1)); // Cache hit
store.invalidate("user:1");
Java
import java.util.*;
import java.util.function.Supplier;
public class CacheAside<K, V> {
private final Map<K, V> cache;
private final Map<K, Long> timestamps;
private final long ttlMs;
public CacheAside(Map<K, V> cache, long ttlMs) {
this.cache = cache;
this.timestamps = new HashMap<>();
this.ttlMs = ttlMs;
}
public V get(K key, Supplier<V> loader) {
long now = System.currentTimeMillis();
if (cache.containsKey(key)) {
if (now - timestamps.getOrDefault(key, 0L) < ttlMs) {
System.out.println("Cache hit: " + key);
return cache.get(key);
}
cache.remove(key);
}
System.out.println("Cache miss: " + key);
V value = loader.get();
cache.put(key, value);
timestamps.put(key, now);
return value;
}
public void invalidate(K key) {
cache.remove(key);
timestamps.remove(key);
}
}
// Uso
CacheAside<String, Map<String, Object>> store =
new CacheAside<>(new HashMap<>(), 60000);
Map<String, Object> user = store.get("user:1", () ->
Map.of("id", 1, "name", "User 1")
);
Explicación
El Patrón Cache-Aside sigue este flujo:
- Lectura: la aplicación verifica el caché — si hay hit, devuelve; si hay miss, continúa.
- Carga: obtiene el dato desde el almacenamiento principal (base de datos, API).
- Almacenamiento: escribe el resultado en el caché con un TTL.
- Invalidación: en escrituras y actualizaciones, elimina la entrada del caché para que la próxima lectura la refresque.
La aplicación es el único punto de control — decide cuándo leer del caché, cuándo recurrir al almacenamiento y cuándo invalidar. El coste es que tu código es dueño de la expiración, la invalidación y el manejo de fallos. Dos escrituras muy próximas también pueden dejar una entrada obsoleta hasta la próxima lectura o hasta que el TTL expire, por eso la invalidación en escritura importa más que el propio TTL.
Variantes
| Variante | Descripción | Caso de uso |
|---|---|---|
| Lazy Loading | El cache miss dispara la carga | La más común; evita llenados innecesarios del caché |
| Write-Through | Las escrituras actualizan caché y DB a la vez | Cuando se requiere consistencia fuerte |
| Refresh-Ahead | Refresca de forma proactiva antes de que expire el TTL | Patrones de acceso predecibles |
| Multi-Nivel | L1 (en memoria) + L2 (Redis) + L3 (DB) | Aplicaciones de alta escala |
Lo que funciona
- Siempre define un TTL — los datos obsoletos son peores que un cache miss
- Invalida en las escrituras — elimina la clave del caché tras actualizar la DB para mantener la consistencia
- Usa un circuit breaker alrededor de las fallas de caché — si Redis está caído, ve directo a la DB
- Serializa objetos complejos antes de guardarlos (JSON, protobuf) y versiona tus claves para que un cambio de esquema produzca misses en vez de errores
- Monitorea el cache hit ratio — apunta a >90% en cargas de lectura intensiva
- Precalienta el caché al arrancar para los datos de referencia críticos
Errores comunes
- Olvidar invalidar el caché tras escrituras en la DB, sirviendo datos obsoletos
- Definir un TTL demasiado largo y servir información desactualizada
- No manejar las fallas del proveedor de caché sin romper (ej. conexión a Redis perdida)
- Almacenar demasiados datos en caché, causando presión de memoria o expulsión de claves calientes
- Cache stampede: muchas peticiones golpean un caché frío a la vez y sobrecargan la DB
Solución de problemas
- Datos obsoletos justo tras una escritura: la actualización llegó a la base de datos pero la entrada vieja del caché sigue viva. Elimina la clave en el mismo camino de código que escribe, y trata el TTL como red de seguridad, no como estrategia.
- Pico de tráfico tras un deploy o vaciado de caché: un caché frío convierte N misses concurrentes en N consultas idénticas a la base de datos. Usa un lock por clave o single-flight para que solo una petición cargue mientras el resto espera.
- Redis inalcanzable: cada búsqueda se convierte en un camino lento hacia la base de datos. Envuelve las llamadas al caché con un timeout corto, trata los errores como misses y deja que un circuit breaker deje de golpear un Redis muerto.
- Hit ratio bajo y estancado: las claves expiran demasiado rápido, el conjunto de trabajo no cabe en memoria o se está cacheando el dato equivocado. Revisa las estadísticas de expulsión antes de añadir más capacidad.
- Desajuste de serialización tras un deploy: cambió la forma almacenada y la deserialización falla. Versiona las claves de caché (
user:v2:1) para que las entradas viejas hagan miss en vez de romperse.
Puntos clave
- Cache-aside mantiene la lectura simple: consulta el caché, ante un miss lee del almacén y llena el caché.
- Combínalo con un TTL y con invalidación en escrituras — el TTL solo deja una ventana de datos obsoletos, y la invalidación sola se rompe la primera vez que se pierde un evento.
- Su punto débil es el stampede de caché frío; planifica un lock o single-flight antes de necesitarlo.
- Prefiérelo sobre write-through cuando dominan las lecturas y el caché debe seguir siendo opcional — si el caché cae, la aplicación sigue funcionando, solo que más lenta.
Temas avanzados
Escenario: Cache-Aside para una API de productos
// Cache-Aside: leer del caché, ante un miss leer de la DB y llenar el caché
class ProductCache {
constructor(private redis: RedisClient, private db: Pool, private ttlSec: number = 300) {}
async getProduct(id: string): Promise<Product | null> {
// 1. Intentar caché
const cached = await this.redis.get(`product:${id}`);
if (cached) return JSON.parse(cached);
// 2. Cache miss: leer de la DB
const res = await this.db.query("SELECT * FROM products WHERE id = $1", [id]);
if (res.rows.length === 0) return null;
const product = res.rows[0];
// 3. Llenar el caché
await this.redis.setex(`product:${id}`, this.ttlSec, JSON.stringify(product));
return product;
}
async updateProduct(id: string, data: Partial<Product>): Promise<void> {
// 1. Actualizar la DB
await this.db.query("UPDATE products SET name=$1, price=$2 WHERE id=$3", [data.name, data.price, id]);
// 2. Invalidar el caché
await this.redis.del(`product:${id}`);
}
async deleteProduct(id: string): Promise<void> {
await this.db.query("DELETE FROM products WHERE id = $1", [id]);
await this.redis.del(`product:${id}`);
}
}
Estrategias de invalidación comparadas:
| Estrategia | Descripción | Pros | Contras |
|---|---|---|---|
| TTL | Expira tras N segundos | Simple | Datos obsoletos hasta expirar |
| Write-through | Escribe en caché al escribir en DB | Siempre consistente | Latencia en escrituras |
| Write-around | Escribe solo en DB, carga el caché al leer | Escrituras rápidas | Primera lectura lenta |
| Write-back | Escribe en caché, asíncrono a DB | Escrituras rápidas | Riesgo de pérdida de datos |
| Explícita | Invalida en update/delete | Control total | Requiere código manual |
El flujo en un paso: las lecturas van caché → miss → base de datos → llenar caché; las escrituras actualizan la base de datos y eliminan la clave del caché en vez de actualizarla. El TTL queda como red de seguridad para los eventos de invalidación que se escapen. Para una implementación completa con Redis y protección anti-stampede, mira la receta de cache-aside con Redis.
Errores comunes en producción
- Cachear sin TTL confiando solo en la invalidación — un evento perdido sirve datos obsoletos indefinidamente.
- Actualizar el caché en las escrituras en vez de eliminar la clave — quedan dos caminos de escritura que mantener sincronizados.
- Tratar el caché como infraestructura obligatoria: si la app lanza error cuando Redis cae, cache-aside se vuelve un lastre en vez de una optimización.
- Serializar objetos con claves sin versión y luego desplegar un cambio de esquema que rompe la deserialización.
- Cachear datos por usuario o por tenant bajo claves compartidas — una fuente clásica de fugas de datos.
- Ignorar el caso del stampede hasta que el primer pico de tráfico tras un deploy tumba la base de datos.
- Llenar el caché con valores que nadie vuelve a leer — un hit ratio bajo significa que el caché añade latencia en vez de quitarla.
Preguntas frecuentes
¿Cuál es la diferencia entre Cache-Aside y Read-Through?
En Cache-Aside, la aplicación controla la lógica de caché: verificar, cargar, llenar, invalidar. En Read-Through, el proveedor de caché obtiene los datos de la base de datos de forma transparente mediante un loader. Cache-Aside es más explícito y portable; Read-Through oculta la fontanería detrás de la capa de caché.
¿Cómo prevengo los cache stampedes?
Usa un lock por clave para que solo una petición llegue a la base de datos mientras las demás esperan — un lock distribuido en Redis funciona, o reutiliza la promesa en curso en JavaScript (single-flight). La expiración temprana probabilística también ayuda: refresca las claves calientes un poco antes de que acabe el TTL para que los misses nunca lleguen todos a la vez.
¿Debería cachear escrituras (Write-Through) o invalidarlas (Cache-Aside)?
Invalidar en la escritura es más simple y seguro — el caché se autocorrige en la próxima lectura. Write-Through actualiza caché y base de datos a la vez, lo que mantiene las lecturas consistentes pero acopla cada escritura al caché. Úsalo solo cuando las lecturas obsoletas sean inaceptables.
¿Cuándo debería evitar Cache-Aside?
En cargas con muchas escrituras donde las entradas rotan antes de ser releídas, cuando se exige consistencia fuerte en cada lectura, o en aplicaciones pequeñas donde la capa extra añade fontanería sin beneficio medible.
¿Qué TTL debería usar?
Parte de cuánta obsolescencia tolera el dato: segundos para precios o inventario, minutos para perfiles de usuario, horas para datos de referencia. No hay un valor universal — un TTL corto combinado con invalidación en escrituras gana a un TTL largo a secas.
¿Qué pasa si el caché se cae?
Diseña para ello: trata los errores de caché como misses con un timeout corto y deja que la base de datos absorba la carga. Es la principal ventaja frente a Read-Through — la aplicación degrada en vez de fallar. Solo asegúrate de que la base de datos soporte todo el tráfico de lectura sin el caché absorbiéndolo.
Recursos Relacionados
Patron Read-Through Cache
Una capa de cache transparente que intercepta peticiones de lectura, obtiene del origen de datos en caso de miss y puebla el cache automaticamente.
PatternPatron Write-Through Cache
Escribe sincronamente en cache y backing store para que el cache siempre tenga los datos mas recientes sin invalidacion basada en TTL.
PatternPatrón Cache Invalidation
Estrategias para mantener frescos los datos en caché: expiración TTL, invalidación explícita, write-through y evicción basada en eventos.
PatternPatron Two-Level Cache
Combina un cache L1 en memoria con un cache L2 distribuido para reducir latencia de hot keys manteniendo consistencia entre instancias.
RecipeImplementar el patron Cache-Aside con Redis
Usa el patron cache-aside para leer y escribir datos a traves de Redis, manejando cache misses, lecturas obsoletas e invalidacion write-through
GuideCaching a Nivel Aplicación
Implementar caches en memoria, distribuidos e hibridos en la capa de aplicacion. Cubre caches LRU, TTL, estrategias multi-tier, sizing, thread safety y patrones de produccion para Python, Java y Node.js.