Readstore frente a llamadas síncronas entre servicios
· 6 min de lectura
En cuanto un sistema se parte en servicios aparece la misma pregunta: cuando el servicio A necesita datos que son del servicio B, ¿se los pide en el momento o se guarda una copia?
La respuesta por defecto suele ser preguntar: una llamada HTTP, y si va lenta, una caché delante. Es lo más sencillo de construir y funciona bien al principio. El problema aparece cuando esas llamadas se encadenan.
En una plataforma e-commerce de más de dos millones de peticiones al mes, una de las decisiones que más mejoró la estabilidad entre servicios fue construir un readstore interno: una copia local, de solo lectura, de los datos que otros servicios necesitaban consultar constantemente. Este post no va de cómo se implementa. Va de cuándo compensa hacerlo y de qué se paga a cambio.
El problema: las cadenas síncronas
Una petición de usuario que atraviesa varios servicios de forma síncrona tiene dos problemas que crecen con cada salto.
La latencia se suma. Si el servicio de catálogo llama al de precios, y este al de stock, el usuario espera la suma de los tres, más la red entre ellos. El percentil 99 es peor todavía, porque basta con que uno de los tres tenga un mal momento.
La disponibilidad se multiplica a la baja. Si cada servicio está disponible el 99,9 % del tiempo, una petición que necesita los tres a la vez está disponible, como mucho, el 99,7 %. Cada dependencia síncrona que añades es una forma más de que tu servicio falle por algo que no es culpa suya.
Usuario ──▶ Catálogo ──HTTP──▶ Precios ──HTTP──▶ Stock
│ │ │
latencia = L1 + L2 + L3
disponibilidad = A1 × A2 × A3
A esto se suma un tercer efecto, menos visible: el acoplamiento operativo. Si Precios se cae, Catálogo se cae con él. Los dos equipos ya no pueden desplegar, escalar ni tener incidentes de forma independiente, que era justo el motivo de separarlos.
Las opciones
1. Llamada síncrona más caché
Es la primera reacción, y muchas veces es suficiente. La caché reduce la latencia media y absorbe parte de los fallos del servicio remoto.
Pero tiene límites claros. Una caché solo ayuda con lo que ya ha visto: un fallo de caché sigue dependiendo del servicio remoto. Los TTL obligan a elegir entre datos frescos y protección. Y cuando la caché se vacía (un deploy, un reinicio, una clave nueva), todo el tráfico vuelve de golpe al servicio de origen, justo cuando menos lo puede aguantar.
2. Readstore alimentado por eventos
El servicio dueño del dato publica un evento cada vez que este cambia. El servicio que lo consume escucha esos eventos y mantiene su propia copia, con la forma exacta que necesita para sus consultas.
Precios ──evento "precio actualizado"──▶ cola/bus ──▶ proyector ──▶ Readstore
│
Usuario ──▶ Catálogo ──consulta local────────────────────────────────┘
En el momento de la petición ya no hay llamada remota: la lectura es local. Si Precios se cae, Catálogo sigue respondiendo con los últimos datos que conoce. Y cuando Precios vuelve, los eventos pendientes ponen la copia al día.
3. Base de datos compartida
Que los dos servicios lean de las mismas tablas. Es la opción más rápida de construir, y la descarto casi siempre: convierte el esquema de un servicio en un contrato público que nadie puede cambiar sin coordinarse con todos los demás. Es un monolito distribuido con más latencia.
Comparativa
| Síncrona + caché | Readstore por eventos | BD compartida | |
|---|---|---|---|
| Latencia de lectura | Variable (aciertos/fallos) | Baja y estable | Baja |
| Si el dueño se cae | Falla al fallar la caché | Sigue funcionando | Depende de la BD |
| Frescura del dato | Hasta el TTL | Segundos de retraso | Inmediata |
| Coste de construcción | Bajo | Medio-alto | Muy bajo |
| Acoplamiento | Medio (contrato API) | Bajo (contrato de eventos) | Alto (esquema) |
El precio: lo que no te cuentan del readstore
Consistencia eventual
La copia va siempre un poco por detrás del original. Normalmente son milisegundos o segundos, pero si la cola se atasca pueden ser minutos.
La pregunta útil no es “¿es aceptable la consistencia eventual?”, sino “¿en qué pantallas es aceptable?”. Mostrar un listado con un precio que cambió hace cinco segundos suele ser perfectamente tolerable. Cobrar ese precio, no. La regla que sigo: el readstore sirve para mostrar; las decisiones se toman contra el dueño del dato.
Mantener la proyección
Un readstore no es una caché que se llena sola. Es código que hay que mantener:
- Reprocesado. Si la proyección tenía un bug, o necesitas un campo nuevo, tienes que poder reconstruirla desde cero. Eso exige poder volver a leer los eventos o hacer una carga inicial desde el dueño.
- Versionado de eventos. El evento es ahora un contrato entre equipos. Añadir campos es fácil; renombrarlos o quitarlos requiere convivir con dos versiones durante un tiempo.
- Orden y duplicados. Los eventos pueden llegar dos veces o desordenados. La proyección tiene que ser idempotente y saber descartar un evento más antiguo que el dato que ya tiene.
- Observabilidad. Necesitas saber cuánto retraso lleva la copia. Un readstore desactualizado sin que nadie lo sepa es peor que una llamada que falla de forma visible.
Quién es el dueño del dato
Este es el punto que más se olvida. Con un readstore, el dato sigue teniendo un único dueño. La copia es de solo lectura y nadie la modifica salvo el proyector. Si un servicio empieza a escribir en su copia “porque es más rápido”, acabas con dos fuentes de verdad que divergen, y eso es mucho peor que cualquier problema de latencia.
Cuándo lo elegiría y cuándo no
Lo elegiría cuando:
- Un servicio lee constantemente datos de otro, y esa lectura está en el camino crítico de las peticiones de usuario.
- El dato cambia mucho menos de lo que se lee.
- La pantalla tolera unos segundos de retraso.
- Quiero que un servicio siga funcionando aunque su dependencia esté caída.
- Ya existe, o tiene sentido crear, una infraestructura de eventos.
No lo elegiría cuando:
- La lectura necesita el valor exacto de este instante (saldos, cobros, reservas de stock en el checkout).
- El volumen de llamadas es bajo y una caché con un buen TTL lo resuelve.
- El equipo no tiene todavía la madurez operativa para mantener colas, reprocesados y alertas de retraso. Un readstore mal mantenido da más problemas que los que resuelve.
- Los servicios se separaron sin un motivo claro, y lo que de verdad toca es plantearse si deberían ser uno solo.
La idea de fondo
Un readstore cambia latencia y disponibilidad por consistencia y complejidad operativa. No es una mejora gratuita: es un intercambio. Compensa cuando la dependencia síncrona está en el camino crítico y el negocio tolera unos segundos de retraso en la lectura.
Bien aplicado, el efecto se nota en lo que más cuesta conseguir en un sistema distribuido: que el fallo de un servicio deje de convertirse automáticamente en el fallo de los demás.