En una arquitectura de microservicios, la independencia de los servicios no es solo cuestión de código: también atañe a los datos. El principio de base de datos por servicio establece que cada microservicio debe ser el dueño exclusivo de sus datos y nadie más puede tocarlos directamente. Esta regla, sencilla de enunciar, dinamita muchas costumbres heredadas del mundo monolítico: ya no hay un JOIN que cruce todas las tablas, ni una transacción única que abarque toda la operación. A cambio, ganamos servicios verdaderamente autónomos, desplegables y escalables por separado. En esta lección estudiaremos qué implica este patrón, qué problemas crea (consultas y transacciones repartidas), y las técnicas para convivir con ellos: sharding, replicación y la separación de vistas mediante CQRS.

Contenido

  1. El patrón Database per Service
  2. El problema de las consultas entre servicios
  3. El problema de las transacciones entre servicios
  4. Escalado de datos: sharding y replicación
  5. CQRS para construir vistas de lectura
  6. Síntesis: cómo encajan las piezas

  1. El patrón Database per Service

La idea central es que cada servicio posee su propia base de datos y es la única vía de acceso a esos datos. Otro servicio que necesite información debe pedirla a través de la API, nunca consultando la base ajena.

┌──────────────────┐        ┌──────────────────┐
│ Servicio Pedidos │        │ Servicio Clientes│
│   ┌──────────┐   │        │   ┌──────────┐   │
│   │  BD       │   │        │   │  BD      │   │
│   │ pedidos   │   │        │   │ clientes │   │
│   └──────────┘   │        │   └──────────┘   │
└────────┬─────────┘        └─────────┬────────┘
         │   API (HTTP / eventos)     │
         └────────────────────────────┘

Beneficios:

  • Acoplamiento débil: un servicio puede cambiar su esquema sin romper a los demás.
  • Libertad tecnológica: Pedidos puede usar PostgreSQL y Clientes, MongoDB (persistencia políglota).
  • Aislamiento de fallos y escalado independiente: la carga de un servicio no afecta a la base del otro.

El precio es que perdemos las dos grandes comodidades del monolito: las consultas que cruzan datos y las transacciones que los abarcan. Veámoslas.

  1. El problema de las consultas entre servicios

En un monolito, mostrar "los pedidos de Ana con su nombre y email" es un JOIN trivial:

-- Esto SOLO funciona en un monolito con una base única
SELECT c.nombre, c.email, p.id, p.total
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
WHERE c.id = 1042;

En microservicios este JOIN es imposible: clientes y pedidos viven en bases distintas, posiblemente con motores distintos. Hay tres estrategias para resolverlo:

2.1 Composición en la API (API Composition)

Un servicio (o el gateway) llama a varios servicios y une los resultados en memoria:

public DetallePedidoDto detalle(long pedidoId) {
    Pedido p = pedidosClient.obtener(pedidoId);       // llamada al servicio Pedidos
    Cliente c = clientesClient.obtener(p.clienteId()); // llamada al servicio Clientes
    return new DetallePedidoDto(
        c.nombre(), c.email(), p.id(), p.total());    // "JOIN" hecho en código
}

Sencillo, pero cada consulta implica varias llamadas de red y no escala bien para listados grandes o filtros complejos (el clásico problema "N+1" multiplicado por la latencia de red).

2.2 Replicación de datos de referencia

El servicio que necesita el dato mantiene una copia local de solo lectura del dato ajeno, sincronizada mediante eventos. Por ejemplo, Pedidos guarda una copia del nombre del cliente, actualizada cuando Clientes publica un evento ClienteActualizado.

2.3 CQRS con vista materializada

Construir una base de lectura dedicada que ya tiene los datos combinados (lo veremos en el apartado 5).

  1. El problema de las transacciones entre servicios

En un monolito, confirmar un pedido y descontar el stock era una transacción ACID única. En microservicios, "pedido" y "stock" están en bases distintas: no existe una transacción que abarque ambas. Las transacciones distribuidas clásicas (2PC, two-phase commit) existen, pero son frágiles, lentas y mal vistas en arquitecturas modernas porque bloquean recursos y reducen la disponibilidad.

La solución recomendada es el patrón Saga (estudiado en el Módulo 5): se descompone la operación en una secuencia de transacciones locales, cada una en su servicio, coordinadas por eventos. Si un paso falla, se ejecutan transacciones de compensación que deshacen los anteriores.

Crear Pedido (Pedidos)  --ok-->  Reservar Stock (Inventario)  --ok-->  Cobrar (Pagos)
      ▲                                  │ falla
      └──── Compensar: cancelar pedido ◄─┘

La consecuencia conceptual más importante: renunciamos a la consistencia inmediata a cambio de consistencia eventual. Durante un instante el sistema puede estar "a medias" (pedido creado, stock aún no descontado), y debemos diseñar la aplicación para tolerarlo.

// Paso local + publicación de evento, dentro de UNA transacción local
@Transactional
public void crearPedido(ComandoCrearPedido cmd) {
    Pedido pedido = Pedido.nuevo(cmd);
    repoPedidos.guardar(pedido);                 // transacción LOCAL del servicio Pedidos
    publicador.publicar(new PedidoCreado(pedido.id(), pedido.lineas()));
    // El servicio Inventario reaccionará a PedidoCreado en SU propia transacción
}

Aquí solo se garantiza atomicidad dentro del servicio Pedidos. La coordinación con Inventario ocurre de forma asíncrona vía el evento PedidoCreado. Para que guardar y publicar no se desincronicen se usa el patrón Transactional Outbox, que escribe el evento en la misma transacción local.

  1. Escalado de datos: sharding y replicación

Cuando los datos de un servicio crecen, dos técnicas (complementarias) ayudan a escalar.

4.1 Replicación

Se mantienen copias de los mismos datos en varios nodos. Normalmente uno es el primario (acepta escrituras) y los demás son réplicas de solo lectura.

# Configuración conceptual de replicación primario-réplica
database:
  primario:
    host: db-primario.interno      # recibe todas las ESCRITURAS
    rol: read-write
  replicas:
    - host: db-replica-1.interno   # sirven LECTURAS, descargan al primario
      rol: read-only
    - host: db-replica-2.interno
      rol: read-only
  • Ventajas: reparte la carga de lectura, mejora la disponibilidad (si cae el primario, una réplica puede promocionarse).
  • Coste: las réplicas pueden ir ligeramente retrasadas (replication lag), introduciendo consistencia eventual en las lecturas.

4.2 Sharding (particionado horizontal)

Se divide el conjunto de datos entre varios nodos según una clave de partición (shard key). Cada nodo guarda un subconjunto distinto; ningún nodo tiene todo.

Estrategia de sharding Cómo reparte Ventaja Riesgo
Por rango Por intervalos de la clave (A-M, N-Z) Consultas por rango eficientes Puntos calientes (hotspots)
Por hash hash(clave) % nº de shards Reparto uniforme Consultas por rango caras
Por directorio Tabla de búsqueda explícita Flexible El directorio es un cuello de botella
shard = hash(cliente_id) % 4
cliente_id=1042 -> shard 2  →  nodo C
cliente_id=2001 -> shard 1  →  nodo B

Diferencia esencial: la replicación copia los mismos datos (escala lecturas y da resiliencia); el sharding divide datos distintos (escala escrituras y volumen). En sistemas grandes se combinan: cada shard tiene además sus réplicas.

  1. CQRS para construir vistas de lectura

CQRS (Command Query Responsibility Segregation) separa el modelo de escritura (comandos que cambian estado) del modelo de lectura (consultas). Lo introdujimos en el Módulo 5; aquí lo aplicamos al problema de las consultas distribuidas.

La idea: en lugar de pelearnos con JOIN imposibles entre servicios, construimos una vista materializada de solo lectura que ya combina los datos, alimentada por los eventos que publican los servicios.

// Proyector: escucha eventos y mantiene una vista de lectura desnormalizada
@Component
public class ProyectorResumenPedidos {

    private final RepositorioVistaPedido vista;

    @EventListener
    public void on(PedidoCreado e) {
        // Crea/actualiza una fila combinada lista para consultar
        vista.guardar(new ResumenPedido(e.pedidoId(), e.clienteId(), "PENDIENTE"));
    }

    @EventListener
    public void on(ClienteRenombrado e) {
        // Mantiene el nombre del cliente actualizado en la vista
        vista.actualizarNombreCliente(e.clienteId(), e.nuevoNombre());
    }
}

Qué conseguimos:

  • La consulta "pedidos con nombre del cliente" se resuelve leyendo una sola tabla ya combinada (la vista ResumenPedido), sin llamadas entre servicios.
  • El modelo de lectura está desnormalizado y optimizado para las consultas reales de la interfaz.
  • A cambio, la vista se actualiza de forma asíncrona: es eventualmente consistente respecto a los datos de origen.
-- Consulta sobre la vista materializada: rápida y sin JOINs entre servicios
SELECT cliente_nombre, pedido_id, estado
FROM resumen_pedidos
WHERE cliente_id = 1042;

CQRS no es gratis: duplica datos y añade la complejidad de mantener los proyectores. Úsalo cuando la asimetría entre lecturas y escrituras lo justifique, no por defecto.

Errores Comunes y Consejos

  • Compartir la base entre servicios "solo para esta consulta". Es la puerta de entrada al monolito distribuido: rompe la autonomía y reintroduce acoplamiento por la base de datos. Prohibido.
  • Intentar transacciones ACID globales con 2PC. Sacrifican disponibilidad y escalabilidad. Prefiere Sagas y consistencia eventual.
  • Elegir mal la shard key. Una clave con distribución desigual (por ejemplo, "país" cuando el 90% son de un solo país) crea hotspots que anulan el beneficio del sharding.
  • Olvidar el replication lag. Si lees de una réplica justo después de escribir en el primario, puedes no ver tu propio cambio. Para "read-your-writes" lee del primario o usa lecturas consistentes.
  • No diseñar las compensaciones de la Saga. Cada paso necesita su acción inversa; si la olvidas, un fallo deja el sistema en estado inconsistente sin remedio automático.
  • Consejo: la consistencia eventual debe ser una decisión explícita y comunicada al negocio, no un efecto colateral silencioso.

Ejercicios

Ejercicio 1. Explica por qué un JOIN entre la tabla pedidos (servicio Pedidos) y la tabla clientes (servicio Clientes) es imposible en una arquitectura de base de datos por servicio, y nombra dos formas de resolver la necesidad de combinar esos datos.

Ejercicio 2. Tienes 100 millones de clientes y la base de un servicio no da abasto en escrituras. ¿Replicación o sharding? Propón una shard key razonable y explica un riesgo de tu elección.

Ejercicio 3. Describe, paso a paso, cómo una Saga gestiona la operación "crear pedido → reservar stock → cobrar" cuando el cobro falla.

Soluciones

Solución 1. Es imposible porque ambas tablas residen en bases de datos distintas, gobernadas por servicios distintos y posiblemente con motores diferentes; el motor de una base no puede ejecutar un JOIN contra tablas que no posee. Dos soluciones: (a) API Composition (llamar a ambos servicios y unir en memoria) y (b) CQRS con vista materializada (mantener una vista de lectura ya combinada, alimentada por eventos). También vale la replicación local de datos de referencia.

Solución 2. Sharding, porque el problema es de volumen de escrituras y la replicación solo escala lecturas. Una shard key razonable es hash(cliente_id), que reparte de forma uniforme. Riesgo: las consultas que abarcan rangos de clientes o agregaciones globales se vuelven caras, porque deben consultar todos los shards y combinar resultados (scatter-gather).

Solución 3. (1) Servicio Pedidos crea el pedido en estado PENDIENTE (transacción local) y publica PedidoCreado. (2) Servicio Inventario reserva el stock (transacción local) y publica StockReservado. (3) Servicio Pagos intenta cobrar y falla, publicando CobroRechazado. (4) Inventario reacciona a CobroRechazado ejecutando la compensación: libera el stock reservado. (5) Pedidos reacciona marcando el pedido como CANCELADO. El sistema vuelve a un estado consistente sin haber usado una transacción global.

Conclusión

Has aprendido que la base de datos por servicio es la pieza que hace verdaderamente autónomos a los microservicios, a costa de renunciar a los JOIN y a las transacciones globales. Para convivir con ello dominas ya un conjunto de técnicas: composición en la API y CQRS con vistas materializadas para las consultas, Sagas y consistencia eventual para las transacciones, y replicación más sharding para escalar. Todas comparten un hilo común: hacer más lecturas y hacerlas rápidas. Y cuando hablamos de acelerar lecturas, la herramienta por excelencia es la caché, con sus propias estrategias y, sobre todo, su difícil problema de invalidación. Eso es justo lo que veremos en la última lección del módulo: Caché y Estrategias de Invalidación.

Curso de Arquitectura de Aplicaciones

Módulo 1: Fundamentos de la Arquitectura de Aplicaciones

Módulo 2: Principios y Tácticas de Diseño

Módulo 3: Estilos y Patrones Arquitectónicos

Módulo 4: Arquitecturas Distribuidas y Microservicios

Módulo 5: Arquitecturas Dirigidas por Eventos y Mensajería

Módulo 6: Diseño Dirigido por el Dominio (DDD)

Módulo 7: Datos y Persistencia

Módulo 8: Arquitectura en la Nube y Despliegue

Módulo 9: Calidad, Seguridad y Observabilidad

Módulo 10: Evolución, Gobernanza y Casos Prácticos

© Copyright 2026. Todos los derechos reservados