La lección anterior terminó con una pregunta sin responder: la bicicleta 417 tiene su matrícula, su modelo y su estado en PostgreSQL, y también copiados dentro de miles de documentos de MongoDB. La estación 12 tiene once columnas en un motor y una ficha de treinta campos en el otro. Nadie ha dicho cuál manda.

Esta lección responde a esa pregunta, y añade dos piezas más al sistema: Redis para la disponibilidad en tiempo real y las sesiones de la aplicación, y Elasticsearch para la búsqueda de estaciones por nombre y dirección. Cuatro motores para un solo servicio municipal de bicicletas.

Es importante decir desde el principio qué tipo de lección es esta. No rediseña nada: el esquema relacional de 08-01 y las colecciones de 08-02 se dan por hechos y no se tocan. Lo que se trata aquí es lo que ninguna de las dos lecciones anteriores podía tratar por separado — el reparto y la coherencia: qué dato vive en qué motor, quién es la fuente de la verdad, con qué retraso se propagan los cambios, qué sigue funcionando cuando una pieza se cae, y cómo se opera un sistema con cuatro almacenes en lugar de uno.

Y trata también la pregunta que casi nunca aparece en las presentaciones de arquitectura: cuándo desmontar todo esto. Porque la mayoría de las arquitecturas políglotas que existen no deberían existir, y saber reconocerlo forma parte del oficio.

Contenido

  1. La persistencia políglota, llevada a la práctica
  2. El precio real, y por qué la respuesta por defecto sigue siendo una sola base de datos
  3. La arquitectura de VallBici
  4. La tabla maestra de decisión: quién manda sobre cada dato
  5. Los cuatro patrones de sincronización
  6. La coherencia en la práctica: 4 bicis en la app, 3 al llegar
  7. Modos de fallo y degradación elegante
  8. Operación: copias coordinadas, monitorización y coste de equipo
  9. Cuándo desmontar la arquitectura políglota
  10. Errores Comunes y Consejos
  11. Ejercicios
  12. Conclusión

  1. La persistencia políglota, llevada a la práctica

El concepto se introdujo en 03-04 con la definición de Martin Fowler:

Persistencia políglota: usar deliberadamente varios motores de almacenamiento dentro de un mismo sistema, eligiendo para cada tipo de dato el que mejor lo gestiona, en lugar de forzar todo en un único motor.

Allí era una idea. Aquí es un sistema en producción, y la diferencia entre las dos cosas es enorme. En la idea, cada dato "vive donde mejor encaja" y todo suena razonable. En el sistema real aparecen cuatro preguntas que la definición no menciona y que son el 90 % del trabajo:

  1. ¿Quién es la fuente de la verdad de cada dato? Cuando dos motores discrepan, uno de los dos tiene razón por definición, y hay que haberlo decidido antes de que ocurra.
  2. ¿Con qué latencia se propaga un cambio? No es "instantáneo". Es 50 ms, o 3 segundos, o 15 minutos, y esa cifra tiene que ser una decisión de diseño con un requisito detrás.
  3. ¿Qué pasa si una copia se pierde? Un almacén derivado que se puede reconstruir en 20 minutos es una cosa; uno que contiene el único ejemplar de un dato es otra completamente distinta.
  4. ¿Qué sigue funcionando cuando una pieza cae? Cuatro motores significan cuatro formas independientes de que el sistema falle, y un sistema donde cualquiera de las cuatro lo tumba entero es peor que uno con una sola base de datos.

Esta lección es, en el fondo, esas cuatro preguntas respondidas para VallBici.

  1. El precio real, y por qué la respuesta por defecto sigue siendo una sola base de datos

Antes del diagrama bonito, la factura. En 03-04 la enumeramos; ahora la cuantificamos con lo que cuesta de verdad.

Coste Con un motor Con cuatro motores
Sistemas que actualizar y parchear 1 4, con calendarios distintos
Copias de seguridad a diseñar y probar 1 4, y coordinarlas entre sí
Sistemas de permisos y auditoría 1 4, con modelos incompatibles
Modos de fallo independientes 1 4, más los fallos de la sincronización
Competencias en el equipo 1 profunda 4, o 2 profundas y 2 superficiales
Depurar «este dato está mal» Mirar una tabla Averiguar en cuál de los cuatro se torció y cuándo
Restaurar a un instante coherente Un PITR Problema abierto (apartado 8)
Onboarding de una persona nueva Días Semanas

Ese penúltimo punto merece énfasis. Con un solo motor, "restaurar el sistema a las 11:39" es una operación resuelta. Con cuatro, la copia de PostgreSQL de las 11:39, la de MongoDB de las 11:41 y el índice de Elasticsearch de las 11:20 no describen el mismo instante del mundo, y hay que decidir qué significa "coherente" en ese contexto.

La respuesta correcta por defecto sigue siendo una sola base de datos. No por conservadurismo: porque el coste de la segunda pieza es permanente y el beneficio suele ser puntual.

Las señales que sí justifican añadir un motor

Un motor nuevo se justifica cuando puedes rellenar esta frase con datos: "tenemos [número] de [dato] que exige [propiedad], nuestro motor actual da [medida] y necesitamos [medida]". Si no puedes rellenarla, no hay caso. Las señales concretas:

Señal Ejemplo en VallBici Motor que la resuelve
Volumen de escritura que el motor principal no absorbe sin degradarse 272 M puntos GPS/año MongoDB
Latencia exigida un orden de magnitud por debajo de lo alcanzable Disponibilidad en < 5 ms, 60.000 veces/día Redis
Una capacidad funcional que el motor principal no tiene Búsqueda con errores tipográficos y sinónimos Elasticsearch
Estructura genuinamente variable que produce migraciones constantes Detalle de incidencias por tipo MongoDB
Datos efímeros cuya pérdida es irrelevante Sesiones de la app Redis

Y las que no lo justifican, aunque se oigan a menudo: "es lo que usa todo el mundo", "así aprendemos una tecnología nueva", "el relacional no escala" (dicho sin una medida), "queremos ser modernos". Cada una de esas frases cuesta, en un sistema como VallBici, entre 20.000 y 60.000 euros al año en operación y en tiempo de equipo.

  1. La arquitectura de VallBici

flowchart TB
    subgraph clientes["Clientes"]
        APP["App móvil<br/>24.000 personas"]
        OPS["Panel de operaciones<br/>y taller"]
        AYT["Informes del<br/>ayuntamiento"]
    end

    API["API de VallBici<br/>(la que decide a quién pregunta)"]
    APP --> API
    OPS --> API
    AYT --> API

    API --> RD["REDIS<br/>disponibilidad por estación<br/>sesiones (EXPIRE)<br/>reservas de 10 min"]
    API --> PG["POSTGRESQL · fuente de la verdad<br/>abonos · trayectos · cobros<br/>bicicletas · anclajes · taller"]
    API --> MG["MONGODB<br/>telemetria · fichas de estación<br/>incidencias"]
    API --> ES["ELASTICSEARCH<br/>búsqueda de estaciones<br/>por nombre y dirección"]

    PG -->|outbox + publicador<br/>~2 s| RD
    PG -->|outbox + publicador<br/>~2 s| MG
    MG -->|reindexado incremental<br/>~30 s| ES
    PG -->|reindexado incremental<br/>~30 s| ES

    IDM["IDENTIDAD MUNICIPAL<br/>(compartida con BiblioRed)"]
    IDM -.->|OIDC| API
    BIB["BiblioRed<br/>(sistema vecino)"]
    IDM -.->|OIDC| BIB

    style PG stroke-width:3px

Las flechas gruesas van todas en el mismo sentido, y eso no es casualidad: PostgreSQL escribe hacia los demás y nadie escribe hacia PostgreSQL. Un sistema donde las flechas de sincronización forman un ciclo es un sistema donde los datos oscilan, y depurarlo es una pesadilla. Regla: el grafo de sincronización debe ser acíclico.

Qué aporta cada pieza

PostgreSQL — el núcleo transaccional. Lo de 08-01, sin cambios. Es el único motor donde se cobra dinero, el único con integridad referencial y el único con restricciones declarativas. Es la fuente de la verdad de todo lo que tiene consecuencias legales o económicas.

MongoDB — telemetría, fichas e incidencias. Lo de 08-02, sin cambios.

Redis — disponibilidad, sesiones y reservas. La pieza nueva, y la que más ilustra el criterio. La app pregunta "¿cuántas bicis hay en cada una de las 60 estaciones?" 60.000 veces al día. En PostgreSQL eso es un SELECT sobre estaciones que devuelve 60 filas: costaría unos 2 ms y funcionaría perfectamente. ¿Entonces por qué Redis?

Por dos motivos que sí son números. Primero, esa consulta se dispara cada vez que alguien abre el mapa y cada 10 segundos mientras lo tiene abierto: en hora punta son 400 consultas por segundo contra la misma base que está procesando desbloqueos, y competir por conexiones con la transacción de cobro es exactamente lo que no se quiere. Segundo, y más importante: la reserva de 10 minutos del ejercicio 1 de 08-01 y la sesión de la app son datos que caducan solos, y EXPIRE hace en una línea lo que en PostgreSQL requiere un proceso de limpieza.

# Disponibilidad: un hash por estación
redis> HSET est:12 libres 4 anclajes_libres 20 total 24 ts 1749888000
(integer) 4
redis> HGETALL est:12
1) "libres"           2) "4"
3) "anclajes_libres"  4) "20"
5) "total"            6) "24"
7) "ts"               8) "1749888000"

# Todas las estaciones de golpe, en una sola ida y vuelta
redis> MGET est:1:libres est:2:libres est:3:libres
1) "7"  2) "0"  3) "12"

# Sesión de la app: caduca sola a los 30 días
redis> SET sess:9f3a2b... '{"persona_id":8801,"abono_id":10233}' EX 2592000
OK
redis> TTL sess:9f3a2b...
(integer) 2591994

# Reserva de bicicleta: caduca sola a los 10 minutos
redis> SET resv:bici:417 10233 EX 600 NX
OK
redis> SET resv:bici:417 10999 EX 600 NX      # otra persona intenta reservar la misma
(nil)                                          # ← NX la rechaza: ya está reservada

Ese último bloque es la joya operativa. SET ... NX es una operación atómica de comprobar-y-fijar: la segunda persona recibe nil y sabe que llegó tarde, sin transacciones y sin bloqueos. Y la reserva caduca sola: no hay proceso de limpieza que pueda fallar. Compáralo con la solución del ejercicio 1 de 08-01 —índice único parcial más caducidad implícita en la consulta— y verás la misma regla resuelta con dos herramientas distintas, cada una idiomática en su motor.

Elasticsearch — la búsqueda. El requisito real: alguien escribe "muelle norte", "paseo muele" (con la errata) o "estacion 12" y espera encontrar la estación 12. En PostgreSQL eso es ILIKE '%...%' —que no usa índice, como vimos en 06-03— o pg_trgm, que va bastante bien. Con 60 estaciones, pg_trgm sería suficiente y hay que decirlo. Elasticsearch entra por una razón concreta y a plazo: el plan municipal prevé 200 estaciones y unificar la búsqueda con la de BiblioRed y otros servicios del ayuntamiento en un solo buscador ciudadano. Es la señal "capacidad funcional que el motor principal no tiene" del apartado 2 — tolerancia a erratas, sinónimos, resaltado, relevancia ajustable.

La identidad municipal, compartida con BiblioRed. Vallmar tiene un proveedor de identidad único: la misma persona entra en BiblioRed para reservar un libro y en VallBici para desbloquear una bici con las mismas credenciales. Es un dato compartido entre dos sistemas de la plataforma municipal, y aquí hay que ser muy estricto con una cosa: VallBici no guarda contraseñas ni credenciales. Guarda un subject_id del proveedor de identidad en personas_abonadas y nada más. Si el ayuntamiento cambia de proveedor, VallBici cambia una columna; si VallBici guardara credenciales, un fallo suyo comprometería también BiblioRed.

  1. La tabla maestra de decisión: quién manda sobre cada dato

Este es el documento más importante del sistema. Si un equipo con arquitectura políglota no lo tiene escrito, no tiene arquitectura: tiene cuatro bases de datos.

Conjunto de datos Fuente de la verdad Copias en Latencia de sincronía Si se pierde la copia
Personas abonadas y abonos PostgreSQL No aplica
Trayectos (apertura y cierre) PostgreSQL Mongo (solo el _id) ~2 s Irrelevante
Cobros e importes PostgreSQL No aplica
Inventario de bicicletas PostgreSQL Mongo (matrícula, modelo, tipo) ~2 s Reconstruible desde PG
Ocupación de anclajes PostgreSQL Redis (contador agregado) < 1 s Reconstruible en 200 ms
Sesión de la app Redis Se pierde: hay que volver a entrar
Reserva de 10 minutos Redis Se pierde: la reserva se anula
Telemetría GPS MongoDB Se pierde de verdad
Ficha enriquecida de estación MongoDB ES (nombre, dirección, distrito) ~30 s Reconstruible desde Mongo
Datos básicos de estación (código, anclajes, distrito) PostgreSQL Mongo, Redis, ES ~2 s / ~30 s Reconstruible desde PG
Detalle de incidencias MongoDB Se pierde de verdad
Orden de taller (hecho contable) PostgreSQL Mongo (el orden_taller_id) ~2 s Irrelevante
Índice de búsqueda Elasticsearch (derivado) ~30 s Reconstruible en ~2 min

Tres lecturas que hay que hacer de esta tabla:

Primera: la columna de la izquierda tiene un solo valor por fila, siempre. No existe "las dos". Si dos motores pudieran modificar el mismo dato, harían falta reglas de resolución de conflictos, y eso es un problema mucho más difícil del que nadie quiere tener en un sistema municipal.

Segunda: hay tres filas con "se pierde de verdad". Telemetría, detalle de incidencias, y —parcialmente— sesiones y reservas. Esas son las que necesitan copia de seguridad propia. Todo lo demás es derivado y se reconstruye. Un dato derivado no necesita copia de seguridad; necesita un procedimiento de reconstrucción probado. Confundir las dos cosas hace que se hagan copias caras de lo que no importa y ninguna de lo que sí.

Tercera: la ocupación de anclajes tiene su verdad en PostgreSQL, no en Redis. Es contraintuitivo —Redis es quien la sirve— y es la decisión que sostiene todo el apartado 6. Redis tiene una copia rápida y aproximada; la verdad está en la tabla anclajes, protegida por su clave primaria y su UNIQUE. Cuando las dos discrepan, gana PostgreSQL, sin excepciones.

Un caso especial merece nota: la estación aparece en cuatro motores. Sus datos básicos nacen en PostgreSQL (código, anclajes, distrito), su ficha enriquecida nace en MongoDB (fotos, accesibilidad, horarios), su disponibilidad vive en Redis y su texto buscable en Elasticsearch. Cuatro motores, un solo objeto del mundo. Y funciona porque cada campo tiene un único dueño: nadie edita el número de anclajes desde MongoDB, y nadie edita las fotos desde PostgreSQL. La regla no es "cada entidad en un motor"; es "cada campo, un dueño".

  1. Los cuatro patrones de sincronización

Cómo llega un cambio de PostgreSQL a los demás. Hay cuatro formas de hacerlo y solo una es mala.

Patrón 1 — Escritura dual (y por qué es frágil)

La aplicación escribe en los dos sitios, uno detrás de otro.

await pg.query('UPDATE anclajes SET bicicleta_id = NULL WHERE ...');
await redis.hincrby('est:12', 'libres', -1);      // ← ¿y si falla aquí?
sequenceDiagram
    participant API
    participant PG as PostgreSQL
    participant RD as Redis
    API->>PG: UPDATE ... COMMIT
    PG-->>API: OK
    API->>RD: HINCRBY est:12 libres -1
    Note over API,RD: 💥 la API se cae aquí
    Note over RD: Redis dice 4 bicis · PostgreSQL dice 3 · para siempre

El problema no es que falle: es que falla en silencio y no se recupera. No hay ninguna transacción que abarque los dos motores, así que la incoherencia queda. Y no vale invertir el orden: si se escribe primero en Redis y falla PostgreSQL, Redis anuncia una bici que no se ha desbloqueado.

Se usa igualmente, en un caso muy concreto: cuando la copia es reconstruible y el desfase es tolerable. La disponibilidad en Redis cumple las dos condiciones, así que VallBici sí hace escritura dual hacia Redis… acompañada de un reconstructor periódico que corrige las derivas. Escritura dual sola, sin red de seguridad, es lo que no se hace nunca.

Patrón 2 — Outbox

La idea: si no se puede hacer una transacción entre dos motores, se hace una transacción dentro de uno que incluya la intención de escribir en el otro.

CREATE TABLE outbox (
    evento_id   BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    agregado    VARCHAR(20)  NOT NULL,     -- 'trayecto', 'bicicleta', 'estacion'
    agregado_id BIGINT       NOT NULL,
    tipo        VARCHAR(30)  NOT NULL,     -- 'trayecto_iniciado', 'bici_a_taller'
    payload     JSONB        NOT NULL,
    ts_creado   TIMESTAMPTZ  NOT NULL DEFAULT now(),
    ts_publicado TIMESTAMPTZ
);
CREATE INDEX idx_outbox_pendientes ON outbox (evento_id) WHERE ts_publicado IS NULL;

La transacción de desbloqueo de 08-01 gana un INSERT más, dentro del mismo BEGIN:

BEGIN;
  UPDATE anclajes   SET bicicleta_id = NULL WHERE estacion_id = 12 AND numero = 3;
  UPDATE bicicletas SET estado = 'en_uso'   WHERE bicicleta_id = 417;
  INSERT INTO trayectos (...) VALUES (...) RETURNING trayecto_id;   -- 884213

  INSERT INTO outbox (agregado, agregado_id, tipo, payload) VALUES
    ('trayecto', 884213, 'trayecto_iniciado',
     '{"trayecto_id":884213,"bicicleta_id":417,"matricula":"VB-0417",
       "modelo":"Ciclmar E-Vall","tipo":"electrica","estacion_origen":12}'),
    ('estacion',  12, 'disponibilidad_cambiada', '{"estacion_id":12,"delta":-1}');
COMMIT;

Y un publicador independiente:

-- Se ejecuta cada 500 ms. SKIP LOCKED permite varios publicadores en paralelo (06-02).
BEGIN;
SELECT evento_id, tipo, payload FROM outbox
 WHERE ts_publicado IS NULL ORDER BY evento_id
 FOR UPDATE SKIP LOCKED LIMIT 100;
-- ... escribe en MongoDB y Redis ...
UPDATE outbox SET ts_publicado = now() WHERE evento_id = ANY($1);
COMMIT;

La garantía exacta que da el outbox, dicha con precisión: si la transacción de negocio confirmó, el evento está en la tabla outbox, porque se escribió en la misma transacción. Por tanto se publicará: si el publicador se cae, al reiniciarse lo encuentra pendiente. Lo que no garantiza es que se publique una sola vez: si escribe en MongoDB y muere antes del UPDATE, al reintentar lo publicará de nuevo. Es una entrega al menos una vez, y por eso todas las escrituras destino tienen que ser idempotentes — que es exactamente por lo que en 08-02 usamos el trayecto_id como _id de MongoDB. Las piezas encajan.

Patrón 3 — Captura de cambios (CDC)

En lugar de que la aplicación declare qué ha cambiado, se lee el registro de escritura anticipada del propio motor. Es el mismo WAL de 06-01: el fichero donde PostgreSQL anota cada cambio antes de aplicarlo, y que allí servía para la durabilidad y el PITR. Aquí se le da un segundo uso.

-- Publicación lógica: PostgreSQL emite los cambios de estas tablas
CREATE PUBLICATION vallbici_cdc FOR TABLE anclajes, bicicletas, estaciones;
SELECT pg_create_logical_replication_slot('vallbici_slot', 'pgoutput');

Un conector (Debezium y similares) se suscribe al slot y produce, para cada cambio, un evento con el antes y el después:

{ "op": "u", "source": { "lsn": 47118293, "ts_ms": 1749888000123 },
  "before": { "estacion_id": 12, "numero": 3, "bicicleta_id": 417 },
  "after":  { "estacion_id": 12, "numero": 3, "bicicleta_id": null } }

La ventaja sobre el outbox es que la aplicación no participa. Nadie puede olvidarse de emitir un evento, ni siquiera un UPDATE hecho a mano por una persona administradora a las 3 de la madrugada. El inconveniente es que los eventos son cambios de filas, no hechos de negocio: el evento de arriba dice "el anclaje 3 pasó a null", no "la persona 8801 desbloqueó una bici eléctrica". Reconstruir el significado a partir de las filas es trabajo, y ese trabajo se acopla al esquema: cambiar una columna rompe el consumidor.

Aviso operativo que ha tumbado más de un sistema: un slot de replicación lógica con un consumidor caído hace que PostgreSQL conserve WAL indefinidamente para poder servírselo cuando vuelva. Si el consumidor lleva tres días parado, el disco se llena y el motor se detiene. Un slot olvidado es una bomba de relojería, y su monitorización (pg_replication_slots) es obligatoria.

Patrón 4 — Reconstrucción periódica

El más simple y el más infravalorado: cada cierto tiempo, se recalcula el almacén derivado desde cero o se compara con la verdad.

-- Reconstruir la disponibilidad de las 60 estaciones. Se ejecuta cada 5 minutos.
SELECT e.estacion_id,
       COUNT(*) FILTER (WHERE a.bicicleta_id IS NOT NULL AND a.estado = 'operativo') AS libres,
       COUNT(*) FILTER (WHERE a.bicicleta_id IS NULL     AND a.estado = 'operativo') AS huecos
  FROM estaciones e JOIN anclajes a USING (estacion_id)
 GROUP BY e.estacion_id;
 estacion_id | libres | huecos
-------------+--------+--------
          12 |      4 |     20
          41 |      0 |     20
         ...
(60 filas · 8 ms)

Ocho milisegundos para reconstruir el estado completo de Redis. Con esa cifra sobre la mesa, la conclusión es fuerte: cualquier deriva del contador se corrige sola en menos de cinco minutos y nadie llega a notarlo. Cuando la reconstrucción completa es barata, la sincronización incremental puede permitirse ser imperfecta — y eso simplifica todo lo demás.

Para Elasticsearch, la reconstrucción completa (60 documentos, o 200 en el futuro) tarda unos dos minutos y se hace con índice paralelo y cambio de alias, de modo que la búsqueda no se interrumpe:

$ curl -XPOST localhost:9200/_aliases -d '{"actions":[
    {"remove":{"index":"estaciones_v3","alias":"estaciones"}},
    {"add":   {"index":"estaciones_v4","alias":"estaciones"}}]}'
{"acknowledged":true}

La tabla que hay que recordar

Patrón Consistencia Complejidad Latencia típica Cuándo usarlo
Escritura dual Ninguna garantía; deriva silenciosa Muy baja Inmediata Solo si la copia es reconstruible y hay reconstructor
Outbox Al menos una vez, sin pérdidas Media (tabla + publicador) 0,5-3 s El caso general. La opción por defecto
CDC Al menos una vez, sin pérdidas ni olvidos Alta (conector, slot, operación) 0,1-1 s Muchos consumidores o escrituras fuera de la aplicación
Reconstrucción Convergencia garantizada, con retraso Baja Minutos u horas Red de seguridad de los otros tres, siempre

VallBici usa outbox como mecanismo principal hacia MongoDB y Redis, escritura dual como atajo rápido hacia Redis para la disponibilidad, y reconstrucción periódica de Redis (5 min) y de Elasticsearch (nocturna) como red de seguridad. CDC se descarta hoy: dos consumidores no justifican operar un conector y vigilar un slot. Si mañana el ayuntamiento quiere un almacén analítico y un sistema de alertas leyendo lo mismo, la decisión cambia — y eso también es correcto.

  1. La coherencia en la práctica: 4 bicis en la app, 3 al llegar

Ahora el problema concreto, que es el que la gente sufre.

La app dice que en la estación 12 hay 4 bicicletas. La persona camina siete minutos, llega y hay 3. ¿Es un error del sistema?

No. Y entenderlo bien es la diferencia entre diseñar un sistema distribuido y sufrirlo. Ese dato nunca pudo ser exacto, ni siquiera con un solo motor: entre el instante en que PostgreSQL lo leyó y el instante en que la persona miró la pantalla han pasado 200 ms, y en esos siete minutos de caminata otras personas han desbloqueado bicis. El desfase de Redis (< 1 s) es despreciable frente al desfase intrínseco de siete minutos.

Esa es la formulación útil:

La pregunta no es si el dato está desfasado. Está desfasado siempre. La pregunta es si el desfase que introduce la arquitectura es pequeño comparado con el desfase que ya existía.

Con esa vara de medir, la tabla queda clara:

Dato Desfase intrínseco Desfase que añade la arquitectura ¿Aceptable?
Bicis libres en el mapa Minutos (el tiempo de llegar) < 1 s Sí, holgadamente
Ficha de la estación Días ~2 s
Resultados de búsqueda Días ~30 s
Bicis libres en el momento de desbloquear Cero No. Aquí no cabe desfase
Importe cobrado Cero No

Las dos últimas filas son las que gobiernan el diseño.

Cómo se diseña para que el desfase sea aceptable

1. La reserva de corta duración. Convierte una promesa incierta en un compromiso firme. La persona ve 4 bicis, pulsa "reservar", y a partir de ese momento tiene una bici garantizada durante 10 minutos. La reserva se toma en Redis con SET ... NX EX 600, que es atómica — pero solo después de confirmarla contra PostgreSQL:

-- Se confirma contra la fuente de la verdad, no contra el contador de Redis
BEGIN;
SELECT a.estacion_id, a.numero, a.bicicleta_id
  FROM anclajes a JOIN bicicletas b USING (bicicleta_id)
 WHERE a.estacion_id = 12 AND a.estado = 'operativo' AND b.estado = 'anclada'
 FOR UPDATE OF a SKIP LOCKED LIMIT 1;
COMMIT;

Si PostgreSQL devuelve 0 filas, la app dice "lo sentimos, se acaba de llevar la última" — y actualiza Redis de paso. El contador rápido sirve para pintar el mapa; la verdad se consulta al comprometerse.

2. La confirmación en el desbloqueo. El mismo principio en el momento crítico. La app nunca desbloquea basándose en el contador de Redis. El desbloqueo es la transacción de 08-01, íntegra, contra PostgreSQL, con su FOR UPDATE SKIP LOCKED. Si dos personas piden la última bici, gana una y la otra recibe un error correcto. Redis no participa en esa decisión.

3. Marca de tiempo visible. La app muestra "actualizado hace 3 s". Es una decisión de producto que cambia la percepción: un dato con antigüedad declarada se lee como una estimación, no como una promesa.

sequenceDiagram
    participant P as Persona
    participant API
    participant RD as Redis
    participant PG as PostgreSQL
    P->>API: abrir mapa
    API->>RD: MGET est:*:libres
    RD-->>API: rápido, aproximado (< 5 ms)
    API-->>P: "estación 12 · 4 bicis · hace 3 s"
    P->>API: reservar
    API->>PG: SELECT ... FOR UPDATE SKIP LOCKED
    PG-->>API: bici 417 — verdad confirmada
    API->>RD: SET resv:bici:417 ... NX EX 600
    API-->>P: "bici VB-0417 reservada · 10 min"
    Note over API,PG: El cobro NUNCA sale de PostgreSQL

Por qué la transacción de cobro nunca sale de PostgreSQL

Merece un apartado propio porque es la regla que no admite excepciones.

Cerrar un trayecto toca cuatro cosas —el trayecto, el anclaje, la bicicleta y el cobro— y las cuatro deben cambiar todas o ninguna. Un fallo a mitad que dejara el trayecto cerrado sin cobro es dinero perdido; uno que dejara el cobro sin cerrar el trayecto es un cobro duplicado en el siguiente cierre. La atomicidad de 06-01 no es un lujo aquí: es el requisito.

Y no existe ninguna transacción que abarque cuatro motores. Los protocolos de confirmación en dos fases existen, y en la práctica se evitan: bloquean recursos en todos los participantes hasta que el coordinador decide, y si el coordinador cae, los participantes se quedan bloqueados. Nadie quiere eso en la ruta de cobro de un servicio municipal.

La consecuencia de diseño es directa y hay que respetarla sin excepciones:

Todo lo que participa en una misma transacción de negocio vive en el mismo motor. Si dos datos tienen que cambiar atómicamente, no se reparten. Y si alguien propone repartirlos, la respuesta correcta es rediseñar el reparto, no inventar una transacción distribuida.

Por eso el outbox está en PostgreSQL y no en una cola externa: es la única forma de que "el trayecto se cerró" y "hay que avisar a los demás motores" sean atómicos entre sí.

  1. Modos de fallo y degradación elegante

Cuatro motores son cuatro formas de fallar. La pregunta de diseño no es "¿cómo evitamos que caigan?" —van a caer— sino "¿qué sigue funcionando cuando cada uno cae?".

Un sistema políglota bien diseñado se degrada por capas. Uno mal diseñado se cae entero cuando falla la pieza menos importante, y entonces es objetivamente peor que un monolito con una sola base de datos.

Pieza que cae Qué deja de funcionar Qué sigue funcionando Plan de contingencia Gravedad
Redis Mapa rápido, sesiones, reservas Todo lo demás: desbloqueo, anclaje, cobro, taller, informes La API cae a consultar disponibilidad en PostgreSQL (8 ms, 60 filas); las sesiones caducan y hay que volver a entrar; las reservas se desactivan Baja
Elasticsearch Búsqueda por texto libre Todo lo demás; la lista de estaciones por distrito y el mapa siguen La app cae a filtrar por distrito y a ILIKE sobre las 60 estaciones de PostgreSQL Muy baja
MongoDB Ficha enriquecida, telemetría, detalle de incidencias Desbloqueo, anclaje y cobro: intactos La ficha muestra los datos básicos de PostgreSQL; la telemetría se acumula en la pasarela y se vuelca al volver; las incidencias se abren solo como orden de taller Media
PostgreSQL Desbloquear, anclar, cobrar, dar de alta abonos — el servicio Consulta del mapa (desde Redis, congelado), búsqueda, fichas Modo solo lectura declarado; la app avisa; no se abren trayectos Crítica

La fila de Redis es la que valida la arquitectura. Redis es la pieza que más peticiones atiende y la que menos importa: si cae, el sistema se pone un poco más lento y sigue cobrando. Esa asimetría —la pieza más solicitada es la más prescindible— es señal de un buen reparto. La señal contraria, un motor auxiliar que tumba el cobro, indica que hay datos en el sitio equivocado.

Lo que no se debe hacer cuando cae MongoDB: escribir la telemetría en PostgreSQL "provisionalmente". Suena solidario y es un desastre: crea un segundo camino de escritura sin probar, mete 272 millones de filas al año en la base que sostiene el cobro y deja datos en dos formatos que alguien tendrá que reconciliar. La respuesta correcta es acumular en la pasarela con un límite y descartar lo más antiguo si se llena. La telemetría es valiosa; no vale una caída del cobro.

Y lo que sí se debe hacer cuando cae PostgreSQL: declararlo. Un sistema que sigue aceptando desbloqueos "a ciegas" con la promesa de registrarlos luego regalará bicicletas y cobrará mal. En modo solo lectura, la app muestra el mapa y dice claramente que no se pueden iniciar trayectos. Fallar de forma visible y honesta es una decisión de diseño, no una rendición.

  1. Operación: copias coordinadas, monitorización y coste de equipo

El problema de restaurar a un instante coherente

Cada motor tiene su estrategia de copia:

Motor Estrategia Frecuencia Granularidad de restauración
PostgreSQL Copia base + WAL archivado (PITR) Continua Cualquier instante
MongoDB Snapshot del sistema de ficheros + oplog Cada 6 h + continuo Cualquier instante dentro de la ventana del oplog
Redis Ninguna copia de la disponibilidad; RDB diario de las sesiones Diaria Aproximada, y da igual
Elasticsearch Ninguna: se reconstruye No aplica

Y aquí está el problema que no tiene solución perfecta: restaurar a las 11:39 no significa lo mismo en los cuatro motores. PostgreSQL puede volver exactamente a las 11:39; MongoDB puede volver a las 11:39 si el oplog alcanza; Redis volverá a un estado de ayer que ya no vale.

La estrategia de VallBici, y el razonamiento:

  1. PostgreSQL se restaura al instante exacto. Es la verdad. Todo lo demás se define respecto a él.
  2. MongoDB se restaura a un instante igual o posterior. Si MongoDB tiene telemetría de trayectos que, tras la restauración, PostgreSQL ya no conoce, son documentos huérfanos — molestos pero inofensivos. Al revés (Mongo anterior a PG) faltaría telemetría de trayectos existentes, que se detecta pero no se recupera. Ante la duda, sobra información en los derivados, no falta.
  3. Redis se vacía y se reconstruye. Los 8 ms del apartado 5 hacen que esta decisión sea trivial. Las sesiones se pierden: 24.000 personas vuelven a entrar. Es molesto y es aceptable.
  4. Elasticsearch se reindexa desde cero. Dos minutos.
  5. Se ejecuta una reconciliación completa antes de volver a abrir el servicio, y se registra su resultado.
# Guion de restauración coordinada, resumido
$ pg_ctl stop && pg_restore_pitr --target-time "2026-06-14 11:39:00+02"
$ mongorestore --oplogReplay --oplogLimit 1749893999
$ redis-cli FLUSHALL
$ ./reconstruir_disponibilidad.sh          # 60 estaciones desde PostgreSQL
$ ./reindexar_elasticsearch.sh --alias-swap
$ ./reconciliar.sh --informe /var/log/vallbici/reconciliacion-20260614.txt
[reconciliar] bicicletas huérfanas en MongoDB ......... 3
[reconciliar] trayectos sin telemetría ................ 128
[reconciliar] contadores Redis divergentes ............ 0
[reconciliar] documentos ES ausentes .................. 0
[reconciliar] RESULTADO: divergencias tolerables · servicio listo

Esas 128 trazas de telemetría faltantes son el coste real de la restauración, y está bien que aparezcan en un informe en lugar de descubrirse por casualidad seis meses después.

Monitorización mínima

Con cuatro motores, la monitorización de cada uno por separado es necesaria pero no suficiente. Lo específico de la arquitectura políglota es vigilar lo que hay entre ellos:

Métrica Umbral de alerta Por qué
Antigüedad del evento más viejo sin publicar en outbox > 60 s El publicador está caído o atascado
Filas pendientes en outbox > 5.000 Se acumula más rápido de lo que se publica
Divergencias en la reconstrucción de Redis > 2 estaciones La escritura dual está fallando
Retraso de reindexado de Elasticsearch > 5 min La búsqueda devuelve datos viejos
Retención de WAL por slots de replicación > 5 GB La bomba del apartado 5.3
Huérfanos detectados en la reconciliación nocturna > 10 Algo se está rompiendo despacio

Las tres primeras no las da ningún motor: son propias de la arquitectura y hay que instrumentarlas a mano. Un equipo que monitoriza cuatro motores impecablemente y no vigila la cola del outbox tiene un punto ciego justo donde ocurren los fallos característicos de este diseño.

El coste de equipo

La parte que no aparece en los diagramas. Para VallBici, con cuatro motores, el equipo necesita competencia real en:

  • PostgreSQL: modelado, planes de ejecución, transacciones, PITR. Profunda, no negociable.
  • MongoDB: modelado documental, pipelines de agregación, índices, conjuntos de réplica. Profunda.
  • Redis: estructuras de datos, caducidad, persistencia y sus límites. Superficial basta.
  • Elasticsearch: analizadores, mappings, reindexado sin corte. Media.
  • La sincronización: outbox, idempotencia, reconciliación. Es la competencia que nadie tiene en el currículum y la que más falta hace.

En un equipo pequeño esto significa, en la práctica, que dos personas se convierten en imprescindibles y que las vacaciones de agosto son un riesgo operativo. Es un coste real y hay que ponerlo en la mesa cuando se decide la arquitectura, no descubrirlo después.

  1. Cuándo desmontar la arquitectura políglota

Esta parte casi nunca se escribe, y es la que más dinero ahorra.

Las señales de que sobra un motor

Señal Qué indica
El motor auxiliar guarda menos datos de los que costó desplegarlo Se añadió por entusiasmo, no por necesidad
La mitad de las consultas al motor auxiliar acaban consultando también al principal El reparto está mal: esos datos van juntos
Nadie ha mirado el panel de ese motor en tres meses No está resolviendo ningún problema visible
Cada incidencia empieza con "¿en cuál de los cuatro está el problema?" El coste de depuración supera el beneficio
El proceso de sincronización tiene más código que la funcionalidad que sostiene Clásico. Es el momento de parar
El volumen que justificó el motor se ha estancado muy por debajo de lo previsto La premisa era falsa
Solo una persona del equipo sabe operarlo Riesgo operativo mayor que el beneficio

Aplicado a VallBici, con honestidad: Elasticsearch es el candidato. Se justificó por un plan de 200 estaciones y por un buscador municipal unificado. Si dentro de dos años siguen siendo 60 estaciones y el buscador unificado no se ha hecho, Elasticsearch estará indexando 60 documentos que pg_trgm serviría igual de bien, y habrá costado dos años de operación, actualizaciones y monitorización. La decisión correcta entonces es quitarlo, no defenderlo porque ya está.

Cómo se vuelve atrás, sin drama

El orden importa, y es más fácil de lo que parece si el motor era derivado:

  1. Comprobar que es derivado. Si su fuente de la verdad es otro motor, se puede apagar sin perder nada. Si contiene datos originales, primero hay que migrarlos y ese es otro proyecto.
  2. Implementar el camino alternativo en el motor principal. Para la búsqueda: índice GIN con pg_trgm sobre nombre y direccion.
  3. Ejecutar en paralelo y comparar resultados durante dos o tres semanas. Registrar las consultas donde discrepan y decidir si la diferencia importa.
  4. Cambiar el tráfico, dejando el motor viejo encendido y sincronizado.
  5. Esperar. Dos semanas sin incidencias.
  6. Apagar, y borrar el código de sincronización. Este paso es el que se olvida: dejar el publicador escribiendo en un motor apagado produce errores en el registro que alguien perseguirá durante meses.
-- El camino alternativo para la búsqueda, en PostgreSQL
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_estaciones_busqueda ON estaciones
       USING gin ((nombre || ' ' || direccion) gin_trgm_ops);

SELECT codigo, nombre, direccion,
       similarity(nombre || ' ' || direccion, 'paseo muele') AS score
  FROM estaciones
 WHERE (nombre || ' ' || direccion) %  'paseo muele'
 ORDER BY score DESC LIMIT 5;
 codigo |          nombre           |     direccion      |  score
--------+---------------------------+--------------------+---------
 VB-012 | Estación 12 · Muelle Norte| Paseo del Muelle 14|   0.41
 VB-019 | Estación 19 · Muelle Sur  | Paseo del Muelle 88|   0.39

Con la errata incluida. Para 60 estaciones, esto es suficiente — y ese es exactamente el argumento. Quitar una pieza que sobra no es admitir un error: es la misma capacidad de análisis que la puso, aplicada con datos nuevos.

Errores Comunes y Consejos

Error 1: no tener escrita la tabla del apartado 4. Sin ella, cada persona del equipo tiene su propia idea de qué motor manda, y el día que discrepan se "arregla" el que se mire primero. La tabla de fuentes de la verdad es el documento fundacional de una arquitectura políglota.

Error 2: sincronización bidireccional. Dos motores que se escriben mutuamente producen ciclos, oscilaciones y conflictos que hay que resolver con reglas ad hoc. Grafo acíclico, siempre.

Error 3: repartir una transacción entre dos motores. Si dos datos tienen que cambiar atómicamente, viven en el mismo motor. No hay excepción que valga en un sistema que cobra dinero.

Error 4: escritura dual sin reconstructor. Funciona el 99,9 % de las veces, y el 0,1 % restante deja una incoherencia permanente que nadie detecta. Toda escritura dual necesita su red de seguridad.

Error 5: creer que "eventual" significa "pronto". La consistencia eventual (03-04) garantiza convergencia si no hay más escrituras y si el mecanismo funciona. Un publicador caído significa "nunca". Por eso la antigüedad del outbox es una alerta.

Error 6: hacer copia de seguridad de los datos derivados y no de los originales. Vimos que Redis y Elasticsearch no necesitan copia; MongoDB, para la telemetría y las incidencias, sí. Confundirlo cuesta caro justo el día en que importa.

Error 7: dejar un slot de replicación lógica sin consumidor. El disco se llena y el motor se detiene. Es el fallo más tonto y más frecuente de las arquitecturas con CDC.

Consejo 1: empieza con un motor y añade el segundo cuando tengas el número. El número: cuántos datos, qué latencia hace falta, qué da tu motor actual. Sin número, no hay caso.

Consejo 2: escribe el procedimiento de reconstrucción antes que el de sincronización. Si sabes reconstruir el derivado en minutos, la sincronización incremental puede fallar sin consecuencias graves. Es lo que hace tolerable toda la arquitectura.

Consejo 3: haz que las escrituras derivadas sean idempotentes desde el primer día. _id con significado, upsert, $max para las marcas de tiempo. La entrega "al menos una vez" es lo único que puedes conseguir; la idempotencia es lo que la hace inofensiva.

Consejo 4: prueba las caídas. Apaga Redis en un entorno de pruebas y comprueba que se puede desbloquear una bicicleta. La tabla del apartado 7 es una hipótesis hasta que se ejecuta.

Consejo 5: revisa la arquitectura una vez al año con la tabla del apartado 9 delante. Las arquitecturas no se simplifican solas.

Ejercicios

Ejercicio 1 — El histórico de precios y el panel público

El ayuntamiento quiere un panel público en tiempo real —una web abierta a la ciudadanía— con: bicis libres por estación (actualizado cada 10 s), trayectos del día en curso, y el mapa de calor del mes anterior. Lo consultarán unas 3.000 personas al día, con picos al publicarse en prensa.

  1. Decide qué motor sirve cada uno de los tres datos y justifícalo con la tabla del apartado 4.
  2. ¿Hay que añadir alguna pieza nueva? Argumenta con el criterio del apartado 2.
  3. Escribe qué pasa con el panel cuando cae cada uno de los cuatro motores.

Ejercicio 2 — Un evento nuevo en el outbox

El taller quiere que, cuando una bicicleta se retira a taller en PostgreSQL, la ficha de esa bici en MongoDB y el contador de Redis se actualicen automáticamente, y que la app deje de ofrecerla.

  1. Escribe la transacción de PostgreSQL que retira la bici y emite el evento de outbox.
  2. Escribe lo que hace el publicador en MongoDB y en Redis, garantizando idempotencia.
  3. Explica qué ocurre si el publicador procesa el mismo evento dos veces, y qué ocurre si lo procesa con 40 minutos de retraso.

Ejercicio 3 — La identidad compartida con BiblioRed

El ayuntamiento quiere que, al darse de alta en VallBici, se detecte si la persona ya es socia de BiblioRed para ofrecerle una tarifa combinada. BiblioRed tiene su propio PostgreSQL, separado del de VallBici.

  1. Enumera tres formas de resolverlo y elige una, justificando con los criterios de la lección.
  2. Explica por qué no se debe hacer que VallBici consulte directamente la base de datos de BiblioRed.
  3. Indica quién es la fuente de la verdad de "esta persona es socia de BiblioRed" y qué pasa si la copia se desfasa.

Soluciones

Solución 1

1. El reparto:

Dato del panel Motor Justificación
Bicis libres cada 10 s Redis Es exactamente el caso para el que está: lectura masiva, dato aproximado, desfase < 1 s frente a un desfase intrínseco de minutos
Trayectos del día en curso PostgreSQL, con caché de 60 s Es un agregado sobre la fuente de la verdad. 3.000 consultas al día con caché de un minuto son ~1.440 consultas reales: irrelevante
Mapa de calor del mes anterior MongoDB, precalculado El pipeline con $unwind tarda segundos y no puede ejecutarse por petición. Se calcula una vez al mes con $merge y el panel lee el resultado

2. Ninguna pieza nueva. Aplicando el criterio: ¿hay un volumen que los motores actuales no absorban? 3.000 visitas diarias son ~0,03 peticiones/segundo de media y quizá 20/s en pico. Redis atiende decenas de miles por segundo. ¿Una latencia inalcanzable? No. ¿Una capacidad funcional que falte? No. Sin número que lo justifique, no se añade nada.

Lo que sí hay que añadir es una caché de respuesta HTTP de 10 segundos delante del panel: convierte el pico de prensa en una consulta cada 10 segundos, sea cual sea el número de visitas. Es la intervención con mejor relación beneficio/coste y no es una base de datos.

3. Degradación del panel:

Cae Panel
Redis Las bicis libres se sirven desde PostgreSQL (8 ms); el panel sigue entero
Elasticsearch Sin efecto: el panel no busca texto
MongoDB Desaparece el mapa de calor; los otros dos bloques siguen. Si el resultado precalculado se copia a PostgreSQL al generarlo, ni eso
PostgreSQL Se congelan los trayectos del día; el mapa de bicis libres sigue desde Redis, con aviso de "datos no actualizados"

Nótese que la última fila describe un panel público que sigue funcionando visualmente durante una caída del núcleo. Para un panel de imagen municipal, eso vale mucho.

Solución 2

1. La transacción:

BEGIN;
  UPDATE bicicletas SET estado = 'taller'
   WHERE bicicleta_id = 417 AND estado = 'anclada';   -- si está en uso, 0 filas: abortar

  UPDATE anclajes SET bicicleta_id = NULL
   WHERE bicicleta_id = 417
  RETURNING estacion_id;                               -- 12

  INSERT INTO ordenes_taller (bicicleta_id, tipo, motivo)
  VALUES (417, 'averia', 'Freno trasero sin recorrido')
  RETURNING orden_id;                                  -- 30412

  INSERT INTO outbox (agregado, agregado_id, tipo, payload) VALUES
   ('bicicleta', 417, 'bici_a_taller',
    '{"bicicleta_id":417,"orden_id":30412,"estacion_id":12,
      "motivo":"Freno trasero sin recorrido","ts":"2026-06-14T07:02:11Z"}');
COMMIT;

El AND estado = 'anclada' es la guarda: si la bici está en un trayecto abierto, el UPDATE afecta a 0 filas y la aplicación debe abortar en lugar de retirar una bici que alguien está usando. El disparador de 08-01 se encarga de bicis_disponibles; el evento de outbox no lo repite.

2. El publicador:

// MongoDB — idempotente por el filtro sobre orden_taller_id, que es único
db.incidencias.updateOne(
  { orden_taller_id: NumberLong(30412) },
  { $setOnInsert: {
      bicicleta: { id: 417, matricula: "VB-0417", tipo: "mecanica", modelo: "Norvent Urbana2" },
      tipo: "frenos", estado: "en_taller", gravedad: 3,
      reportada_por: { canal: "operario" },
      ts_apertura: ISODate("2026-06-14T07:02:11Z"),
      estacion: 12, detalle: {}, esquema_v: 1 } },
  { upsert: true }
);
# Redis — idempotente porque fija un valor, no lo incrementa
redis> SREM est:12:bicis 417
(integer) 1
redis> HSET est:12 libres 3 ts 1749884531
(integer) 0

La clave está en el tipo de operación. updateOne con upsert y $setOnInsert produce el mismo resultado se ejecute una vez o cien. SREM sobre un conjunto es idempotente por naturaleza. Y el contador se fija con HSET a un valor absoluto, no con HINCRBY -1: un HINCRBY aplicado dos veces restaría dos. Esta es la regla general: en un sistema con entrega "al menos una vez", fija valores absolutos, no incrementos.

3. Los dos escenarios:

  • Procesado dos veces: nada cambia. El upsert encuentra la incidencia existente y $setOnInsert no toca nada; el SREM devuelve 0 y el HSET fija el mismo valor. Es exactamente el comportamiento que la idempotencia debe dar.
  • Procesado con 40 minutos de retraso: durante 40 minutos la app siguió ofreciendo la bici 417 en la estación 12. Y no pasa nada grave, porque el desbloqueo confirma contra PostgreSQL: quien la intente coger recibe un error correcto ("esta bicicleta no está disponible"), porque bicicletas.estado = 'taller' ya está puesto desde el minuto cero. El desfase produce una mala experiencia, no una incoherencia. Ese es el diseño funcionando: la verdad se consultó en el momento del compromiso. Lo que sí debe ocurrir es que la alerta de antigüedad del outbox (> 60 s) haya saltado a los 40 minutos.

Solución 3

1. Tres formas y una elegida:

Opción Cómo Valoración
A — Consulta directa a la BD de BiblioRed VallBici abre una conexión al PostgreSQL de BiblioRed Descartada (ver punto 2)
B — API de BiblioRed consultada en el momento del alta VallBici llama a GET /socios/{subject_id} durante el alta Correcta, pero acopla el alta a la disponibilidad de BiblioRed
C — Atributo en el proveedor de identidad municipal BiblioRed publica el atributo socio_bibliored: true en el perfil OIDC; VallBici lo recibe en el token Elegida

La C gana por tres razones alineadas con la lección: el dato llega en el momento del inicio de sesión sin llamada adicional; el proveedor de identidad ya es la pieza compartida entre los dos sistemas, así que no se añade ninguna dependencia nueva; y si BiblioRed cae, VallBici sigue funcionando con el último valor del token. La B sigue siendo necesaria como respaldo para el caso de personas que se dan de alta presencialmente sin pasar por el proveedor de identidad.

2. Por qué no la consulta directa. Cuatro razones, en orden de peso:

  1. Acopla los esquemas. El día que BiblioRed renombre una columna, VallBici deja de funcionar, y nadie en el equipo de BiblioRed sabrá que eso podía pasar. Es la peor forma de dependencia que existe: invisible para quien la rompe.
  2. Rompe el modelo de permisos. VallBici necesitaría credenciales de lectura sobre la base de datos de socios de la biblioteca. Un incidente de seguridad en VallBici pasaría a ser un incidente en BiblioRed.
  3. Rompe la tabla de fuentes de la verdad. Un sistema que lee la base de datos de otro sistema no tiene un contrato, tiene una costumbre.
  4. Datos personales. Que VallBici pueda leer qué libros lee una persona es un tratamiento de datos que nadie ha autorizado. Un contrato de API expone socio_bibliored: true y nada más.

3. La fuente de la verdad es BiblioRed, y el proveedor de identidad es un intermediario que distribuye una copia. VallBici guarda esa copia en personas_abonadas.socio_bibliored con la fecha en que la recibió.

Si la copia se desfasa —la persona se da de baja de BiblioRed y VallBici no se entera— el efecto es que sigue disfrutando de la tarifa combinada hasta la siguiente renovación. Es un coste económico pequeño y acotado, y la mitigación adecuada no es sincronizar más rápido, sino caducar la copia: el atributo tiene validez de 30 días y se refresca en cada inicio de sesión. Si lleva más de 30 días sin refrescarse, la tarifa combinada no se aplica en la siguiente renovación. Es la misma idea que el EXPIRE de Redis, aplicada a un dato de negocio: cuando dependes de una copia ajena, ponle fecha de caducidad.

Conclusión

Has visto una arquitectura políglota completa y, sobre todo, has visto lo que cuesta. Cuatro motores para VallBici: PostgreSQL con el núcleo transaccional de 08-01, MongoDB con la telemetría, las fichas y las incidencias de 08-02, Redis con la disponibilidad, las sesiones y las reservas, y Elasticsearch con la búsqueda. Y compartiendo identidad con BiblioRed, porque en una plataforma municipal los sistemas son vecinos, no islas.

Lo que hay que llevarse no es el diagrama. Son cinco ideas, y funcionan igual con cuatro motores que con dos.

La primera: cada campo tiene un dueño. No cada entidad, cada campo. La estación 12 vive en cuatro motores y no hay conflicto porque nadie edita el número de anclajes desde MongoDB ni las fotos desde PostgreSQL. La tabla del apartado 4 es el documento fundacional del sistema, y un equipo que no la tiene escrita no tiene arquitectura: tiene cuatro bases de datos.

La segunda: lo que cambia junto, vive junto. El trayecto, el anclaje, la bicicleta y el cobro cambian en la misma transacción, así que están en el mismo motor. No hay transacción que abarque cuatro sistemas, y las que lo intentan cuestan más de lo que resuelven. Por eso la tabla outbox está dentro de PostgreSQL: es lo único que hace atómicos "se cerró el trayecto" y "hay que avisar a los demás".

La tercera: el desfase no es el enemigo; el desfase desconocido lo es. Que la app diga 4 bicis y haya 3 no es un fallo: es un dato con una antigüedad declarada, y el desfase de un segundo que añade Redis es despreciable frente a los siete minutos que tarda una persona en llegar. Lo que sí es innegociable es que el momento del compromiso —reservar, desbloquear, cobrar— se resuelva contra la fuente de la verdad. Los contadores rápidos pintan pantallas; las decisiones se toman contra PostgreSQL.

La cuarta: se diseña para las caídas, no contra ellas. Que Redis pueda caerse sin impedir un solo cobro es la prueba de que el reparto está bien hecho. Y esa tabla de contingencias es una hipótesis hasta que apagas Redis en pruebas y compruebas que se puede desbloquear una bicicleta.

Y la quinta, que es la más incómoda: la respuesta correcta por defecto sigue siendo una sola base de datos. Cada motor añadido cuesta operación, competencia en el equipo, modos de fallo y semanas de aprendizaje para quien llegue nuevo. VallBici tiene cuatro porque hay números detrás: 272 millones de puntos GPS anuales, 60.000 consultas diarias de disponibilidad, una búsqueda con erratas que ILIKE no hace. Si esos números no existieran, la arquitectura correcta sería PostgreSQL con jsonb, pg_trgm y una caché — y decirlo en voz alta en la reunión de diseño es más valioso que cualquier diagrama de cuatro cajas. Por eso el apartado 9 existe: saber desmontar una pieza cuando sus números dejan de sostenerla es la misma competencia que la puso, ejercida un año después con datos nuevos.

Con esta lección se cierra el módulo 8 y se cierra el recorrido práctico del curso. Empezaste en 01-01 preguntándote qué es una base de datos y por qué no basta con una hoja de cálculo; terminas repartiendo los datos de un servicio municipal entre cuatro motores y sabiendo justificar cada reparto, cada duplicación y cada sincronización. Entre medias has normalizado hasta la FNBC y has desnormalizado a propósito, has visto una actualización perdida con tus propios ojos en dos terminales, has leído planes de ejecución y has diseñado documentos pensando primero en las consultas. Eso es, con bastante precisión, el trabajo. Lo que queda por delante no es más temario: es criterio, y el criterio se hace con proyectos y con errores propios. El módulo 9 reúne el material para seguir por tu cuenta —09-01 los libros que conviene tener a mano, 09-02 los cursos y tutoriales para profundizar en cada motor, y 09-03 las herramientas con las que se trabaja de verdad—. Elige uno de los tres casos de VallBici, móntalo en tu máquina y rómpelo: es la única parte del curso que no te podemos dar hecha.

Fundamentos de Bases de Datos

Módulo 1: Introducción a las Bases de Datos

Módulo 2: Bases de Datos Relacionales

Módulo 3: Bases de Datos No Relacionales

Módulo 4: Diseño de Esquemas

Módulo 5: Normalización

Módulo 6: Transacciones, Rendimiento y Seguridad

Módulo 7: Ejercicios Prácticos

Módulo 8: Casos de Estudio

Módulo 9: Recursos Adicionales

© Copyright 2026. Todos los derechos reservados