Dashboard de KPIs con varias agregaciones pesadas de MongoDB (sumas, agrupaciones por rango de fechas, joins entre colecciones). Recalcular todo en cada request revienta el tiempo de respuesta. Cachear con TTL fijo (ej. "cada 5 minutos recalcula") tampoco sirve: o el usuario ve datos desactualizados después de facturar algo, o recalculas de más cuando nadie cambió nada.
El problema con TTL fijo
TTL corto = gastas CPU recalculando aunque nada haya cambiado en los datos fuente. TTL largo = el usuario factura algo, entra al dashboard, y ve el número viejo durante minutos. Ninguna de las dos es aceptable cuando el KPI se usa para decisiones operativas del día.
La idea: invalidar por contenido, no por tiempo
En vez de preguntar "¿ya pasó suficiente tiempo?", pregunto "¿cambiaron los datos que alimentan este KPI?". Genero un fingerprint (hash) de las condiciones que afectan el cálculo, y solo recalculo si ese fingerprint cambió desde la última vez.
// colección: control_kpi
{
_id: "ventas_mes_actual",
fingerprint: "a3f5c9...",
valor: { total: 45231000, cantidad: 312 },
calculadoEn: ISODate("2026-07-04T10:15:00Z")
}
El fingerprint se construye a partir de metadatos baratos de calcular, no del recálculo completo:
public String calcularFingerprint(String tipoKpi, LocalDate desde, LocalDate hasta) {
long countFacturas = facturaDao.countByRango(desde, hasta);
Date ultimaModificacion = facturaDao.findUltimaModificacionEnRango(desde, hasta);
String base = tipoKpi + "|" + countFacturas + "|" + ultimaModificacion.getTime();
return DigestUtils.sha256Hex(base);
}
countByRango y findUltimaModificacionEnRango son consultas casi gratis (usan índices existentes) comparadas con la agregación completa del KPI. Si el conteo de documentos no cambió y la última modificación es la misma, el fingerprint es idéntico, y con eso basta para saber que el dato cacheado sigue siendo válido.
El flujo de lectura
public KpiResult obtenerKpi(String tipoKpi, LocalDate desde, LocalDate hasta) {
String fingerprintActual = calcularFingerprint(tipoKpi, desde, hasta);
Document cache = controlKpiCollection.find(Filters.eq("_id", tipoKpi)).first();
if (cache != null && fingerprintActual.equals(cache.getString("fingerprint"))) {
return KpiResult.fromCache(cache); // hit, cero recálculo
}
KpiResult resultado = recalcularKpiCompleto(tipoKpi, desde, hasta); // miss, agregación pesada
controlKpiCollection.replaceOne(
Filters.eq("_id", tipoKpi),
new Document("_id", tipoKpi)
.append("fingerprint", fingerprintActual)
.append("valor", resultado.toDocument())
.append("calculadoEn", new Date()),
new ReplaceOptions().upsert(true)
);
return resultado;
}
Por qué esto gana sobre TTL en la práctica
Si nadie factura nada en una hora, el dashboard sirve el mismo resultado cacheado durante esa hora entera sin recalcular ni una vez — algo que un TTL de 5 minutos jamás lograría. Y si alguien factura hace 10 segundos, el próximo request ya detecta el fingerprint distinto y recalcula de inmediato — algo que un TTL de 5 minutos tampoco lograría. Terminas gastando CPU exactamente cuando hace falta, ni más ni menos.
Cuándo no vale la pena
Si tu consulta de "¿cambió algo" es tan cara como el cálculo completo, no ganas nada — mejor cachea por TTL simple. Esto funciona porque countByRango y findUltimaModificacionEnRango son órdenes de magnitud más baratas que la agregación real.
Resumen para copiar y pegar
- Fingerprint = hash de metadatos baratos (conteo + última fecha de modificación), no del cálculo completo.
- Colección de control con
_idpor tipo de KPI, guarda fingerprint + valor + timestamp. - En cada request: calcula fingerprint actual, compáralo, recalcula solo si difiere.
- Solo vale la pena si detectar cambio es mucho más barato que recalcular.
Top comments (0)