La lección anterior cerró el Módulo 7 con una pregunta pendiente desde el Módulo 1: cuando el monolito Python+PostgreSQL se partió en catalogo, pedidos, inventario, pagos, reparto y analitica, ¿qué hizo que el resultado fuera un sistema y no una colección de seis programas que se llaman por red? Siete módulos han construido las piezas (gRPC y Kafka, sagas y réplicas, Cassandra y Redis, Spark y Flink, Keycloak y Vault, Prometheus y Kubernetes), pero ninguno ha explicado el criterio con el que se decidió dónde cortar, qué datos posee cada servicio, por qué catalogo no consulta la base de datos de inventario aunque técnicamente podría, ni qué se paga por cada una de esas decisiones. Esta lección mira a Kilómetro Cero desde arriba, como arquitectura de microservicios: qué son (y qué no), cómo se trazan las fronteras con un Domain-Driven Design ligero, cómo se resuelven las consultas que cruzan servicios cuando cada uno es dueño de sus datos, cómo se elige entre comunicación síncrona y asíncrona, cómo se migra desde un monolito sin detener el negocio, cómo se organizan los equipos y, sobre todo, cuándo no conviene hacerlo. Termina con el formato que deja escritas esas decisiones: el ADR. El tiempo real (08-02), la nube (08-03) y serverless (08-04) quedan para las lecciones siguientes.

Contenido

  1. Qué son los microservicios y qué no
  2. Las cinco características que los definen
  3. Cómo cortar las fronteras: Domain-Driven Design ligero
  4. Errores de corte
  5. Datos: una base de datos por servicio y sus consecuencias
  6. CQRS: el panel de pedidos de un productor y la vista productos_vista
  7. Comunicación: síncrona, asíncrona, APIs, gateway, BFF y mesh
  8. Patrones de migración: evaluación del plan de estrangulamiento
  9. Organización: equipos, Conway y plataforma interna
  10. Cuándo no usar microservicios y el retorno al monolito modular
  11. Registrar las decisiones: el ADR-001
  12. Errores Comunes y Consejos
  13. Ejercicios
  14. Conclusión

  1. Qué son los microservicios y qué no

En 01-02 se presentó "servicios/microservicios" como un modelo de arquitectura más, junto a cliente-servidor, capas y P2P, y se remitió aquí. La definición operativa es esta: una arquitectura de microservicios descompone una aplicación en servicios pequeños, cada uno alineado con una capacidad de negocio, que se despliegan de forma independiente y se comunican por red mediante contratos explícitos. Lo que la distingue no es el tamaño ni la tecnología, sino tres propiedades que se refuerzan entre sí: independencia de despliegue, propiedad de los datos y fronteras trazadas por el negocio. Conviene situarla frente a sus dos vecinas, con las que suele confundirse.

Aspecto Monolito modular SOA clásica Microservicios
Unidad de despliegue Una (todo el proceso) Pocas, grandes, compartiendo un bus corporativo (ESB) Muchas, pequeñas, una por capacidad
Fronteras Paquetes/módulos dentro del mismo código; se cruzan con llamadas a función Servicios de grano grueso, a menudo por sistema (CRM, ERP) Por capacidad de negocio (bounded context)
Datos Una base de datos compartida Bases compartidas entre servicios; el ESB transforma Una base de datos por servicio; nada compartido
Comunicación Llamadas en proceso Bus centralizado con lógica de orquestación y transformación "Tuberías tontas, extremos listos": HTTP/gRPC, eventos; la lógica en los servicios
Consistencia Transacciones ACID locales Transacciones distribuidas (2PC, 03-05) Consistencia eventual, sagas
Equipos Uno o varios sobre el mismo código Por sistema o por capa Un equipo dueño de cada servicio, de extremo a extremo
Escalado Todo o nada (copias del proceso) Por servicio, pero con el ESB como cuello Por servicio, independiente
Fallo Un error tumba el proceso entero El ESB es un punto único Aislado por servicio (si está bien hecho)
Cuándo encaja Equipos pequeños, dominio aún inestable, latencia crítica Integración de sistemas heredados heterogéneos Varios equipos, dominios claros, necesidad de escalado y despliegue diferenciados

Dos aclaraciones importantes. La primera: el monolito modular no es el enemigo. El monolito de Kilómetro Cero en 01-06 ya tenía seis paquetes por dominio, y esa modularidad fue lo que hizo posible la extracción; un monolito con módulos bien separados y una sola base de datos es una arquitectura legítima y, para muchas empresas, la mejor. La segunda: la SOA de los años 2000 fracasó no por la idea de servicios sino por el ESB (Enterprise Service Bus), que concentraba lógica, transformaciones y orquestación en un componente central que acababa siendo un nuevo monolito, propiedad de un equipo de integración que todos esperaban. Kafka, en Kilómetro Cero, es deliberadamente lo contrario: un log que no transforma ni decide nada (02-04); toda la inteligencia está en los productores y consumidores.

  1. Las cinco características que los definen

Las cinco propiedades siguientes son el examen que cada servicio de Kilómetro Cero tiene que aprobar. Cuando una falla, el sistema se acerca al monolito distribuido, la peor combinación posible: la complejidad de la red sin la independencia que la justificaba.

Característica Qué significa Cómo se cumple en Kilómetro Cero Cómo se rompe
Un servicio = una capacidad de negocio El servicio hace algo que el negocio reconoce con su nombre: "reservar stock", "cobrar", "asignar reparto" inventario es lo que un operador llama "el stock"; reparto es lo que Jordi Sala llama "la flota" Servicios por capa técnica (servicio-base-de-datos, servicio-validaciones)
Dueño de sus datos Solo el servicio lee y escribe su almacenamiento; los demás pasan por su API o por sus eventos km0_inventario solo lo toca inventario; catalogo sabe el stock por stock.actualizado (04-05) Un JOIN de analitica contra las tablas de pedidos; una tabla productos compartida
Despliegue independiente Se puede publicar una versión nueva sin coordinar con nadie ni desplegar a nadie más pedidos 1.15.0 salió en canary (07-05) sin que inventario cambiara Un cambio de inventario que obliga a redesplegar pedidos el mismo día
Equipo dueño Un equipo decide, construye, despliega y opera el servicio (you build it, you run it) El equipo de reparto lleva reparto y está de guardia por él (07-03) Un "equipo de backend" que toca los seis servicios
Fallos aislados Un servicio caído o lento degrada su función, no la plataforma pagos lento abre el circuito (07-04) y el pedido queda "pago pendiente"; el catálogo sigue Llamadas síncronas encadenadas sin timeout: el síntoma 2 de 01-06

Nótese que ninguna característica habla de tamaño en líneas de código. "Micro" es una desafortunada elección de nombre: el tamaño correcto es el que permite cumplir las cinco propiedades con un equipo de tamaño razonable (la regla informal de "un equipo que se alimenta con dos pizzas"). pedidos es probablemente el servicio más grande de Kilómetro Cero, con su orquestador de sagas, su outbox y su repositorio de Cassandra, y está bien así: partirlo en pedidos-creacion y pedidos-consulta no añadiría independencia, solo llamadas de red.

  1. Cómo cortar las fronteras: Domain-Driven Design ligero

La pregunta más difícil de una arquitectura de microservicios no es técnica: es dónde cortar. Cortar mal produce servicios que cambian siempre a la vez, que se llaman en cadena para cualquier operación y que comparten datos por la puerta de atrás. La herramienta conceptual más útil es el Domain-Driven Design (DDD), del que aquí tomamos solo tres ideas, suficientes para trazar fronteras sin adoptar toda su metodología.

Lenguaje ubicuo

Cada dominio tiene un vocabulario que el negocio y el código deben compartir palabra por palabra. Un producto en el catálogo tiene nombre, descripción, fotos y precio; para el inventario es una referencia con unidades disponibles y reservadas en un mercado; para el reparto es un bulto con peso y necesidad de frío. Es el mismo queso-curado, pero con tres significados distintos, y forzar una única clase Producto con los campos de los tres es lo que produce las tablas con 80 columnas del monolito. El lenguaje ubicuo es la señal para detectar fronteras: donde una misma palabra cambia de significado, hay un límite de contexto.

Bounded contexts

Un bounded context (contexto delimitado) es el ámbito en el que un modelo y su lenguaje son válidos y coherentes. Dentro, un término significa una sola cosa; fuera, puede significar otra. Un microservicio bien cortado coincide con un bounded context (o, a veces, con un conjunto pequeño de ellos que un equipo posee juntos). Los seis contextos de Kilómetro Cero, con el término que da nombre a su modelo central:

Contexto Concepto central (agregado) Términos propios Lo que no le pertenece
catalogo Producto publicado ficha, foto, precio de venta, productor, categoría, "disponible" (indicador, no cantidad) La cantidad exacta en stock; el precio pagado en un pedido concreto
pedidos Pedido línea, estado (creado, pagado, rechazado, entregado), importe, saga Qué hay en stock; cómo se cobra; dónde está la furgoneta
inventario Referencia por mercado unidades disponibles/reservadas, reserva con caducidad, umbral de alerta La descripción o la foto; el precio
pagos Cobro pasarela, autorización, reembolso, Idempotency-Key Las líneas del pedido (solo el importe y la referencia)
reparto Ruta y posición repartidor, bulto, franja horaria, posición, asignación El contenido del pedido más allá de peso y frío
analitica Hecho de venta ventas por día/mercado/productor, previsión, alerta de stock (lectura) Nada transaccional: solo lee eventos

Mapa de contextos

El mapa de contextos dibuja las relaciones entre bounded contexts y, para cada una, quién manda sobre el contrato. Las relaciones habituales son: cliente-proveedor (el consumidor negocia el contrato con el proveedor: pedidos pide a inventario un ReservarStock), conformista (el consumidor acepta el modelo del proveedor tal cual: analitica consume los eventos como vienen), capa anticorrupción (el consumidor traduce el modelo ajeno al suyo para no contaminarse: pagos frente a la pasarela externa) y lenguaje publicado (un contrato explícito y versionado que todos conocen: los eventos de pedidos.eventos con su envoltura id_evento/tipo/version/fecha_ms/origen/datos de 02-04).

flowchart LR
    subgraph Web[Clientes]
        NAV[Navegador de Ana]
        APP[App repartidores]
        PAN[Panel productores]
    end
    NAV --> GW[Kong<br/>06-05]
    APP --> GW
    PAN --> GW
    GW --> CAT[catalogo<br/>Producto publicado]
    GW --> PED[pedidos<br/>Pedido]
    GW --> REP[reparto<br/>Ruta y posición]
    PED -- "gRPC ReservarStock<br/>cliente-proveedor" --> INV[inventario<br/>Referencia por mercado]
    PED -- "gRPC Cobrar<br/>cliente-proveedor" --> PAG[pagos<br/>Cobro]
    PAG -- "ACL" --> PAS[(Pasarela externa)]
    PED -. "pedido.creado / pago.confirmado<br/>lenguaje publicado" .-> K[(Kafka<br/>pedidos.eventos)]
    INV -. "stock.actualizado / stock.reservado" .-> K
    K -. conformista .-> CAT
    K -. conformista .-> REP
    K -. conformista .-> ANA[analitica<br/>Hecho de venta]
    REP -. "reparto.posiciones" .-> K2[(Kafka<br/>reparto.*)]
    K2 -.-> ANA

Las flechas continuas son llamadas síncronas y dibujan las dependencias en tiempo de ejecución: si inventario cae, pedidos no puede confirmar (pero sí aceptar, como se vio en 03-05). Las discontinuas son eventos y no crean dependencia en tiempo de ejecución: catalogo sigue funcionando con la última vista que tenga aunque Kafka esté caído. Un mapa con muchas flechas continuas es la primera señal de un corte discutible.

Heurísticas para decidir dónde cortar

Cuando el lenguaje no basta para decidir, cuatro heurísticas ayudan, y conviene aplicarlas todas porque a veces se contradicen:

  1. Cohesión: lo que cambia junto debe vivir junto. La reserva de stock y la liberación de la reserva son la misma capacidad; separarlas en dos servicios obligaría a coordinar cada cambio de reglas en dos sitios.
  2. Tasa de cambio: lo que cambia a ritmos distintos conviene separarlo. pagos cambia poco y con auditoría; catalogo cambia varias veces al día en campaña. Juntarlos impone al catálogo el ritmo de pagos (síntoma 3 de 01-06).
  3. Datos que cambian juntos en la misma transacción: si dos entidades necesitan actualizarse atómicamente con frecuencia, separarlas obliga a una saga por operación. Las unidades disponibles y las reservadas de una referencia se actualizan siempre juntas: mismo servicio, misma fila. El pedido y el stock se actualizan juntos solo al confirmar: aquí sí compensa la saga porque el resto del tiempo son independientes.
  4. Equipos: una frontera de servicio que atraviesa un equipo genera dos servicios que se despliegan siempre a la vez; una que agrupa dos equipos genera peleas por el mismo código. La frontera debe coincidir con la propiedad (apartado 9).

  1. Errores de corte

Los tres errores más frecuentes tienen nombre propio, y merece la pena reconocerlos porque los tres parecen "más microservicios" y son lo contrario.

Servicios anémicos o servicios-entidad. Cortar por tabla: servicio-productos, servicio-clientes, servicio-pedidos, cada uno un CRUD sin lógica. Toda operación de negocio ("confirmar pedido") atraviesa cuatro o cinco de ellos en cadena síncrona, y la lógica acaba en un "servicio orquestador" que es el monolito de siempre con latencia de red. La prueba: si el servicio solo tiene endpoints GET/POST/PUT/DELETE sobre un recurso y ningún verbo de negocio (reservar, cobrar, asignar), es una tabla con API, no una capacidad.

Nanoservicios. Cortar tan fino que el coste de la comunicación supera al de la función: un servicio para calcular el IVA, otro para formatear direcciones. Cada uno necesita despliegue, monitorización, contrato, guardia. Kilómetro Cero decidió en 01-06 que pedidos incluye el cálculo del importe y no un servicio-precios, y en el ejercicio 1 de aquella lección que los cupones simples viven en pedidos, no en un servicio-promociones.

Monolito distribuido. El resultado de romper cualquiera de las cinco características del apartado 2: servicios que comparten base de datos, que deben desplegarse juntos porque comparten un modelo de datos o una librería interna con lógica de negocio, o que se llaman en cadena síncrona para todo. Señales medibles: cada release toca más de tres repositorios; un cambio de esquema de una tabla exige coordinar a dos equipos; la traza de una petición sencilla (07-02) muestra más de cinco saltos síncronos.

  1. Datos: una base de datos por servicio y sus consecuencias

El principio "cada servicio es dueño de sus datos" (01-06, principio 1) es el que más cuesta aceptar porque renuncia a la herramienta más potente del monolito: el JOIN. En el monolito, el panel de la Quesería Montblanc se resolvía con una consulta:

-- En el monolito: una sola consulta sobre una sola base de datos
SELECT p.id, p.fecha, p.estado, l.producto, l.unidades, pr.nombre,
       r.repartidor, r.entrega_prevista
FROM pedidos p
JOIN lineas l     ON l.pedido_id = p.id
JOIN productos pr ON pr.slug = l.producto
LEFT JOIN repartos r ON r.pedido_id = p.id
WHERE pr.productor = 'queseria-montblanc'
ORDER BY p.fecha DESC;

Con pedidos en Cassandra (04-04), catalogo en PostgreSQL y reparto en su propio almacenamiento, esa consulta ya no existe. Hay tres formas de reconstruirla, y elegir bien entre ellas es una decisión de arquitectura tan importante como el corte de los servicios.

Técnica Cómo funciona Cuándo encaja Coste
Composición en la API Un componente (el BFF, o el propio servicio que atiende la petición) llama a varios servicios y une las respuestas en memoria Pocas llamadas, datos pequeños, necesidad de frescura inmediata Latencia = suma de llamadas (o el máximo si son paralelas); la disponibilidad es el producto de las disponibilidades; sin paginación ni filtro cruzado eficientes
Vista materializada por eventos El servicio que necesita la consulta se suscribe a los eventos de los demás y mantiene una copia desnormalizada en su propia base, con la forma exacta de la consulta Consultas frecuentes, filtros y ordenaciones cruzadas, tolerancia a segundos de retraso Consistencia eventual; almacenamiento duplicado; código de proyección que mantener
CQRS Separar formalmente el modelo de escritura (comandos, validaciones, agregados) del modelo de lectura (vistas construidas a partir de los eventos), incluso en almacenes distintos Cargas de lectura y escritura muy diferentes, muchas vistas distintas de los mismos hechos Dos modelos que mantener; la lectura no ve la escritura al instante

Las tres técnicas se apoyan en piezas que el curso ya tiene: la composición usa los clientes gRPC con timeouts y circuit breaker (02-03, 07-04); las vistas materializadas usan el consumidor idempotente y la outbox de 02-05; y CQRS generaliza las vistas materializadas.

Consistencia eventual como norma. En 03-05 la saga sustituyó la transacción global por pasos locales con compensaciones; la consecuencia para las consultas es que, durante unos milisegundos o segundos, el pedido P-2026-000125 existe en pedidos pero aún no en la vista del productor. En una arquitectura de microservicios eso no es un defecto que corregir sino el estado normal, y el diseño lo asume: la interfaz muestra "actualizado hace 3 s", los tests aceptan la ventana (07-06), y la única pregunta que se hace por cada dato es "¿qué retraso tolera quien lo lee?". La tabla de 03-01 de modelos de consistencia por dato es la que se revisa aquí: fuerte solo para el stock disponible y el estado del cobro; eventual para todo lo demás.

  1. CQRS: el panel de pedidos de un productor y la vista productos_vista

El panel del productor como problema de CQRS

Marta Puig, de la Quesería Montblanc, abre su panel y espera ver sus pedidos de los últimos 30 días con producto, unidades, estado y hora prevista de entrega, filtrables por mercado y ordenables por fecha. Los hechos viven en tres servicios: el pedido y sus líneas en pedidos, el nombre del producto en catalogo, la asignación y la hora prevista en reparto. Por composición en la API haría falta leer todos los pedidos (Cassandra no filtra por productor: su clave es el cliente, 04-04), consultar catalogo por cada producto y reparto por cada pedido, y ordenar en memoria: inviable a partir de unos cientos de pedidos.

CQRS lo resuelve separando los dos lados:

  • El lado de comandos sigue como está: pedidos recibe "crear pedido", ejecuta la saga, escribe en Cassandra y publica en pedidos.eventos a través de la outbox. Es el modelo optimizado para escribir correctamente: validaciones, idempotencia, estados.
  • El lado de consultas es un consumidor nuevo dentro de pedidos (o un servicio de consulta propio, si crece) que se suscribe a pedido.creado, pago.confirmado, pago.rechazado y a los eventos de asignación de reparto, y mantiene una tabla pedidos_por_productor en PostgreSQL con exactamente las columnas y el índice que el panel necesita. Es el modelo optimizado para leer rápido.
flowchart LR
    PAN[Panel de Marta] -- "1. POST /pedidos (comando)" --> W[pedidos: lado de escritura<br/>saga, outbox]
    W --> C[(Cassandra<br/>km0_pedidos)]
    W -. "pedido.creado, pago.confirmado" .-> K[(Kafka<br/>pedidos.eventos)]
    REP[reparto] -. "reparto.asignado" .-> K
    K --> P[Proyección<br/>consumidor idempotente]
    P --> V[(PostgreSQL<br/>pedidos_por_productor)]
    PAN -- "2. GET /productores/queseria-montblanc/pedidos (consulta)" --> R[pedidos: lado de lectura]
    R --> V

Los dos lados comparten el nombre pedidos y el equipo, pero no el esquema ni la base de datos: la escritura no sabe que existe la vista, y la vista se puede reconstruir desde cero releyendo el tópico desde el principio (la retención de Kafka de 02-04 es lo que lo permite), lo que también es la forma de añadir una columna nueva al panel sin migración.

Código: la vista productos_vista en catalogo

El mismo patrón, en su forma más simple, es el que catalogo usa para saber si un producto está disponible sin consultar a inventario. En 04-05 el evento stock.actualizado invalidaba la caché de Redis; aquí, además, alimenta una tabla desnormalizada en la base de datos de catalogo, de modo que la ficha del producto y los listados filtrables por disponibilidad se sirven con una sola consulta local.

-- km0/sql/catalogo/productos_vista.sql
-- Vista materializada "a mano": la mantiene el consumidor, no PostgreSQL.
CREATE TABLE IF NOT EXISTS productos_vista (
    slug            TEXT NOT NULL,
    mercado         TEXT NOT NULL,
    nombre          TEXT NOT NULL,           -- copiado del modelo de catalogo
    productor       TEXT NOT NULL,
    precio_cents    INTEGER NOT NULL,
    disponible      BOOLEAN NOT NULL DEFAULT FALSE,
    unidades_aprox  INTEGER NOT NULL DEFAULT 0,  -- "aprox": es eventual por diseño
    version_stock   BIGINT NOT NULL DEFAULT 0,   -- último fecha_ms aplicado de inventario
    actualizado_en  TIMESTAMPTZ NOT NULL DEFAULT now(),
    PRIMARY KEY (slug, mercado)
);
CREATE INDEX IF NOT EXISTS idx_productos_vista_disp ON productos_vista (mercado, disponible, productor);
# km0/servicios/catalogo/proyeccion_stock.py
"""Proyección CQRS: mantiene productos_vista a partir de stock.actualizado.

Reutiliza el consumidor idempotente de 02-05 (tabla mensajes_procesados) y la
invalidación de Redis de 04-05. Solo añade la escritura en la vista.
"""
import json
import psycopg
from confluent_kafka import Consumer
from servicios.comun import logs, metricas
from servicios.catalogo.cache import invalidar_producto   # 04-05

log = logs.obtener("catalogo.proyeccion")
UMBRAL_DISPONIBLE = 1   # una unidad libre ya cuenta como "disponible"


def aplicar(conn: psycopg.Connection, evento: dict) -> None:
    """Aplica un stock.actualizado a la vista. Idempotente y tolerante al desorden."""
    d = evento["datos"]
    with conn.transaction():
        # 1. Deduplicar por id_evento (02-05): si ya se aplicó, salir sin tocar nada.
        ya = conn.execute(
            "INSERT INTO mensajes_procesados (id_mensaje, consumidor) VALUES (%s, 'catalogo.proyeccion') ON CONFLICT DO NOTHING RETURNING 1",
            (evento["id_evento"],)).fetchone()
        if ya is None:
            metricas.contador("km0_proyeccion_duplicados_total").inc()
            return
        # 2. Escribir solo si el evento es más reciente que lo ya aplicado (01-05: ordenación).
        #    Si llega un evento antiguo por un reintento tardío, la condición lo descarta.
        n = conn.execute(
            """
            UPDATE productos_vista
               SET disponible = %(disp)s, unidades_aprox = %(uds)s,
                   version_stock = %(v)s, actualizado_en = now()
             WHERE slug = %(slug)s AND mercado = %(mercado)s AND version_stock < %(v)s
            """,
            {"slug": d["producto"], "mercado": d["mercado"],
             "uds": d["disponible"], "disp": d["disponible"] >= UMBRAL_DISPONIBLE,
             "v": evento["fecha_ms"]}).rowcount
    if n:
        invalidar_producto(d["producto"], d["mercado"])   # la caché de 04-05 sigue siendo válida
    log.info("vista actualizada", producto=d["producto"], mercado=d["mercado"], aplicado=bool(n))


def bucle(conn: psycopg.Connection, consumidor: Consumer) -> None:
    consumidor.subscribe(["pedidos.eventos"])
    while True:
        msg = consumidor.poll(1.0)
        if msg is None or msg.error():
            continue
        evento = json.loads(msg.value())
        if evento["tipo"] != "stock.actualizado":
            continue            # esta proyección ignora los demás tipos
        aplicar(conn, evento)
        consumidor.commit(msg)  # ack después de escribir: at-least-once + idempotencia (02-05)

Explicación de las decisiones que el código encierra:

  • La fila de productos_vista la crea catalogo cuando el productor publica el producto (con disponible = FALSE); la proyección solo la actualiza. Si llega un evento de stock de un producto aún no publicado, el UPDATE no afecta a ninguna fila y se ignora sin error: no es un problema porque, cuando se publique, catalogo pedirá el stock inicial por ConsultarStock (02-03).
  • version_stock < fecha_ms es la defensa contra el desorden: con 6 particiones en pedidos.eventos y clave = id de pedido, dos eventos del mismo producto pueden ir a particiones distintas y llegar invertidos. Sin esta condición, un evento viejo pisaría a uno nuevo. Es el mismo razonamiento del SeguimientoRepartidor de 01-05.
  • El commit del offset va después de la transacción: si el proceso muere en medio, el evento se reprocesa y mensajes_procesados lo descarta.
  • unidades_aprox lleva "aprox" en el nombre a propósito: es una decisión de lenguaje ubicuo. Nadie en catalogo debe usar ese número para decidir si se puede vender; eso es ReservarStock en inventario con consistencia fuerte.

Código: el bounded context como paquete con API pública

La otra mitad de "dueño de sus datos" es que el resto del código no pueda saltarse la frontera. Dentro de un servicio Python, la convención de Kilómetro Cero es que cada bounded context es un paquete con un único módulo público, api.py, y todo lo demás privado. Quien importe servicios.inventario.repositorio desde pedidos está cometiendo el mismo error que un JOIN cruzado.

# km0/servicios/inventario/api.py
"""API pública del bounded context `inventario`.

Es el ÚNICO módulo de este paquete que otros contextos pueden importar (en el
mismo proceso, durante la migración) o exponer por gRPC (contratos/inventario.proto).
Todo lo que no está aquí es detalle de implementación y puede cambiar sin aviso.
"""
from dataclasses import dataclass
from servicios.inventario import _dominio, _repositorio, _eventos   # privados: prefijo _

__all__ = ["Reserva", "StockInsuficiente", "reservar_stock", "liberar_reserva", "consultar_stock"]


@dataclass(frozen=True)
class Reserva:
    id_reserva: str
    pedido_id: str
    caduca_en_s: int


class StockInsuficiente(Exception):
    """Error de negocio, no técnico: la saga de 03-05 lo trata como fallo permanente."""


def reservar_stock(id_reserva: str, pedido_id: str, mercado: str, lineas: list[dict]) -> Reserva:
    """Reserva unidades de varias referencias en un mercado, atómicamente.

    Idempotente por id_reserva: repetir la llamada devuelve la misma Reserva.
    Publica stock.reservado y stock.actualizado vía outbox en la misma transacción.
    """
    with _repositorio.transaccion() as tx:
        if (existente := _repositorio.buscar_reserva(tx, id_reserva)):
            return existente
        for linea in lineas:
            ref = _repositorio.bloquear_referencia(tx, linea["producto"], mercado)  # SELECT ... FOR UPDATE
            _dominio.reservar(ref, linea["unidades"])          # lanza StockInsuficiente
            _repositorio.guardar_referencia(tx, ref)
            _eventos.publicar(tx, "stock.actualizado", {"producto": ref.slug, "mercado": mercado,
                                                        "disponible": ref.disponibles})
        reserva = _repositorio.crear_reserva(tx, id_reserva, pedido_id, lineas, caduca_en_s=900)
        _eventos.publicar(tx, "stock.reservado", {"pedido_id": pedido_id, "id_reserva": id_reserva})
        return reserva


def liberar_reserva(id_reserva: str) -> None:
    """Compensación de la saga (C2 en 03-05). Idempotente: liberar dos veces no duplica stock."""
    with _repositorio.transaccion() as tx:
        reserva = _repositorio.buscar_reserva(tx, id_reserva)
        if reserva is None or reserva.liberada:
            return
        for linea in reserva.lineas:
            ref = _repositorio.bloquear_referencia(tx, linea["producto"], reserva.mercado)
            _dominio.liberar(ref, linea["unidades"])
            _repositorio.guardar_referencia(tx, ref)
            _eventos.publicar(tx, "stock.actualizado", {"producto": ref.slug, "mercado": reserva.mercado,
                                                        "disponible": ref.disponibles})
        _repositorio.marcar_liberada(tx, id_reserva)
        _eventos.publicar(tx, "stock.liberado", {"pedido_id": reserva.pedido_id, "id_reserva": id_reserva})


def consultar_stock(producto: str, mercado: str) -> int:
    """Lectura con consistencia fuerte (va al primario de Patroni). Para la vista de catalogo
    NO se usa esto: se usa la proyección por eventos."""
    return _repositorio.leer_disponibles(producto, mercado)

El servidor gRPC de 02-03 (ReservarStock, ConsultarStock) no hace más que traducir mensajes protobuf a estas tres funciones, y el ServicioInterceptor de 06-04 decide quién puede llamar a cada una. La ventaja de tener el contexto como paquete con API es que durante la migración (apartado 8) el monolito pudo importar servicios.inventario.api en proceso antes de que existiera el servicio remoto, y el cambio a gRPC no tocó a los llamantes.

  1. Comunicación: síncrona, asíncrona, APIs, gateway, BFF y mesh

Síncrona o asíncrona, caso por caso

El Módulo 2 dio las herramientas (gRPC en 02-03, Kafka y RabbitMQ en 02-04, patrones asíncronos en 02-05). Lo que corresponde a esta lección es el criterio para elegir, que se resume en una pregunta: ¿el llamante necesita la respuesta para continuar, y la necesita ahora?

Situación Elección Razón Ejemplo en Kilómetro Cero
El resultado decide el siguiente paso y debe ser fuerte Síncrona (gRPC) La saga no puede avanzar sin saber si hay stock pedidos → inventario.ReservarStock
Una persona espera la respuesta en pantalla Síncrona, con timeout corto y degradación Ana no espera más de 500 ms (SLO de 07-01) catalogo sirviendo la ficha; pedidos aceptando el pedido
Varios interesados en un mismo hecho Asíncrona (evento) El productor no debe conocer a los consumidores pago.confirmado → reparto, analitica, catalogo, Lambda de facturas (08-04)
El receptor puede estar caído sin que importe Asíncrona Desacoplamiento temporal analitica consumiendo todo
Trabajo largo o por lotes Asíncrona (cola o evento) No bloquear al llamante Generar la factura; recalcular previsiones
Alto volumen con orden por clave Asíncrona (Kafka con clave) Orden por partición y replay reparto.posiciones
Notificación al exterior con confirmación del tercero Síncrona hacia fuera, asíncrona hacia dentro La pasarela responde síncrona; el resultado se propaga como evento pagos → pasarela; luego pago.confirmado

La regla práctica de Kilómetro Cero, fijada en el ejercicio 2 de 01-06 y confirmada aquí: síncrono solo cuando la respuesta gobierna la siguiente decisión; todo lo demás, eventos. Cada llamada síncrona añade una dependencia de disponibilidad (la disponibilidad de la cadena es el producto de la de sus eslabones) y de latencia (la suma), y por eso el mapa de contextos del apartado 3 solo tiene dos flechas continuas entre servicios.

API pública frente a API interna

No todas las interfaces son iguales. Una API pública (la que consume el navegador de Ana o la app de repartidores, y en el futuro terceros) es un compromiso a largo plazo: se versiona, se documenta (OpenAPI), se protege en el gateway, se limita por tasa y se mantiene compatible durante meses. Una API interna (gRPC entre pedidos e inventario) tiene pocos consumidores conocidos, cambia con más libertad y su compatibilidad se verifica con las pruebas de contrato de 07-06. Confundirlas lleva o a exponer detalles internos (el id_reserva no le interesa a Ana) o a burocratizar cambios internos.

Versionado de APIs

Toda API pública cambia. La disciplina tiene dos niveles. Los cambios compatibles (añadir un campo opcional en la respuesta, añadir un endpoint, aceptar un parámetro nuevo con valor por defecto) no necesitan versión nueva: el cliente antiguo ignora lo que no conoce. Los cambios incompatibles (renombrar o quitar un campo, cambiar su tipo o su semántica, cambiar códigos de error) exigen una versión nueva coexistiendo con la antigua durante un periodo anunciado. Kilómetro Cero versiona en la ruta (/api/v1, /api/v2), que es la opción más visible y la que Kong enruta sin ambigüedad (06-05).

# km0/servicios/pedidos/api_http.py (fragmento): v1 y v2 servidas por el mismo código
from fastapi import FastAPI, Header
from pydantic import BaseModel

app = FastAPI()


class PedidoV1(BaseModel):
    id: str
    estado: str
    total: float                # en euros, con decimales: la decisión original


class PedidoV2(BaseModel):
    id: str
    estado: str
    total_cents: int            # cambio INCOMPATIBLE: tipo y semántica distintos
    moneda: str = "EUR"
    entrega_prevista: str | None = None   # campo NUEVO opcional: habría sido compatible en v1


def _cargar(pedido_id: str) -> dict:
    ...  # repositorio de Cassandra (04-04); el modelo interno guarda total_cents


@app.get("/api/v1/pedidos/{pedido_id}", response_model=PedidoV1, deprecated=True)
def pedido_v1(pedido_id: str):
    p = _cargar(pedido_id)
    # Adaptador: el modelo interno ya es v2; v1 se DERIVA de él, nunca al revés.
    return PedidoV1(id=p["id"], estado=p["estado"], total=p["total_cents"] / 100)


@app.get("/api/v2/pedidos/{pedido_id}", response_model=PedidoV2)
def pedido_v2(pedido_id: str):
    p = _cargar(pedido_id)
    return PedidoV2(id=p["id"], estado=p["estado"], total_cents=p["total_cents"],
                    entrega_prevista=p.get("entrega_prevista"))

Tres detalles importan más que el código: la versión antigua se marca deprecated y se anuncia una fecha de retirada (cabecera Sunset en la respuesta y aviso a los consumidores registrados en Kong); se mide quién sigue usando v1 (la métrica km0_http_requests_total{ruta="/api/v1/..."} de 07-01 dice cuándo se puede apagar); y el modelo interno nunca se mantiene en dos formas: v1 es una traducción del modelo actual. Para los eventos, el versionado ya se trató en 02-05 (version en la envoltura, compatibilidad hacia atrás en el esquema).

API gateway y BFF

El gateway de 06-05 es el borde de la API pública: enrutado, JWT, rate limiting, X-Request-Id. Desde el punto de vista arquitectónico, su función es que los clientes conozcan un dominio y no seis servicios, y que las preocupaciones transversales vivan en un solo sitio. Lo que no debe hacer es componer respuestas: cuando un cliente necesita datos de varios servicios en una pantalla, el BFF (Backend for Frontend) es el lugar, uno por tipo de cliente, propiedad del equipo de ese cliente. La pantalla de inicio de la app de repartidores (ruta del día, próximos bultos, incidencias) es el caso previsto en 06-05 y que el proyecto final (08-05) sitúa en la arquitectura.

Service mesh: cuándo merece la pena

En 06-04 se vio el mesh como forma de tener mTLS sin código, y en 07-05 como sidecar que da timeouts, reintentos, outlier detection y telemetría RED por configuración; ambas remitieron aquí la decisión. El criterio es de número y heterogeneidad: con seis servicios, todos en Python y con servicios/comun/{resiliencia,metricas,trazas}.py compartidos, el mesh añade 1-2 ms por salto, memoria por sidecar y un plano de control que operar, a cambio de poco que la librería común no dé ya. Compensa cuando aparecen servicios en otros lenguajes (un recomendaciones en Go o Java que no puede usar la librería Python), cuando el número de servicios supera la decena y la uniformidad de políticas se vuelve un problema de gobierno, o cuando la seguridad exige mTLS obligatorio auditable (PeerAuthentication: STRICT) sin fiarse de que cada equipo lo configure bien. Kilómetro Cero lo adopta en la arquitectura final (08-05) por esta última razón y porque el coste de operarlo lo asume la plataforma interna (apartado 9), no los equipos de producto.

  1. Patrones de migración: evaluación del plan de estrangulamiento

En 01-06 se fijó un plan de estrangulamiento (strangler fig) en cinco fases. Ahora, con todas las piezas construidas, es el momento de evaluarlo fase a fase: qué se extrajo, con qué patrón, y qué se aprendió. Antes, los cuatro patrones que se usaron.

  • Strangler fig: se coloca una fachada delante del monolito (en Kilómetro Cero, Kong), se implementa una capacidad en un servicio nuevo y se redirige a él solo esa ruta; el monolito pierde responsabilidades hasta quedar vacío. En ningún momento hay dos sistemas que mantener enteros.
  • Anti-corruption layer (ACL): una capa de traducción entre el modelo nuevo y el antiguo (o uno externo), para que el modelo del servicio nuevo no herede los defectos del monolito. Es la relación "capa anticorrupción" del mapa de contextos.
  • Branch by abstraction: dentro del monolito, se introduce una interfaz (InventarioPuerto) con dos implementaciones, la local y la remota, seleccionable por configuración; se prueba la remota con un porcentaje del tráfico y se retira la local cuando está validada. Permite migrar sin ramas de código de larga duración.
  • Migración de datos con doble escritura y CDC: para mover una tabla del monolito a la base del servicio nuevo sin parada: primero se copia el histórico; después el monolito escribe en ambas (doble escritura, con la nueva como secundaria) o, mejor, un proceso de Change Data Capture (Debezium leyendo el WAL de PostgreSQL, el mismo WAL de 03-04) replica cada cambio al nuevo almacén; se verifica la igualdad; se invierte la dirección (el servicio nuevo es primario y el monolito lee de él); y se apaga la copia antigua.
Fase (01-06) Qué se extrajo Patrones aplicados Lecciones donde se construyó Qué se aprendió
1 catalogo con Redis y km0-fotos Strangler fig por la ruta /api/v1/catalogo; datos copiados en frío (solo lecturas, sin doble escritura) 04-03, 04-05, 06-05 Empezar por lecturas quita riesgo: el retorno al monolito era cambiar una ruta en Kong. El síntoma 1 desapareció antes de tocar nada transaccional
2 pagos aislado ACL frente a la pasarela; branch by abstraction (PagosPuerto local/remoto); Idempotency-Key 02-05, 07-04 La ACL evitó arrastrar al servicio nuevo el modelo de "cobro dentro de la transacción del pedido". El circuit breaker resolvió el síntoma 2 incluso antes de extraer pedidos
3 reparto con Kafka y tiempo real Nuevo tópico reparto.posiciones; la tabla posiciones dejó de escribirse en PostgreSQL (sin migrar histórico: datos que caducan) 02-04, 04-04, 05-04, 08-02 Los datos de serie temporal no se migran, se dejan de escribir en el sitio equivocado. Fue la fase que introdujo el bus, del que luego se colgó todo
4 pedidos e inventario Doble escritura + CDC para pedidos (a Cassandra) y para inventario (a km0_inventario); saga sustituyendo a la transacción; outbox 03-04, 03-05, 04-04, 02-05 La fase más larga y arriesgada: dos migraciones de datos con verificación de igualdad durante tres semanas. Se descubrieron 0,4 % de pedidos con líneas huérfanas en el monolito, que la migración limpió
5 analitica sobre Kafka y HDFS Consumidor conformista de todos los tópicos; lago /km0/eventos; DAG km0_ventas_diarias 04-02, 05-02, 05-03, 05-05 La última porque dependía de que todo estuviera ya en eventos. Las consultas nocturnas contra producción desaparecieron y el síntoma 5 con ellas
Transversal Seguridad, observabilidad, Kubernetes Introducidos desde la fase 1 (Kong, Prometheus, trazas) y ampliados en cada fase Módulos 6 y 7 Lo que no se puso desde el principio (las trazas llegaron en la fase 2) costó el doble de poner después

Una observación sobre el orden: el plan de 01-06 puso reparto (fase 3) antes que pedidos (fase 4) aunque el síntoma 3 (despliegues) fuera el que los equipos sentían cada semana. Fue una decisión correcta por una razón que entonces no se hizo explícita: reparto obligó a montar Kafka, y sin Kafka la fase 4 no habría podido usar la outbox ni las vistas materializadas. El orden de una migración lo dicta la infraestructura que cada fase deja a la siguiente, no solo el dolor que alivia.

  1. Organización: equipos, Conway y plataforma interna

La ley de Conway (1967) dice que un sistema refleja la estructura de comunicación de la organización que lo construye. Es una ley empírica, pero contrastada: si dos equipos deben ponerse de acuerdo para cada cambio, aparecerá una interfaz entre sus componentes; si un equipo posee dos servicios, tenderán a acoplarse porque nadie paga el coste de mantenerlos separados. La consecuencia práctica, a veces llamada maniobra inversa de Conway, es que la organización se diseña para obtener la arquitectura deseada: en 01-06 Kilómetro Cero tenía cuatro equipos (catálogo y productores, pedidos y pagos, reparto, datos), y las fronteras de servicio siguieron esas líneas. pedidos y pagos son dos servicios del mismo equipo, y de hecho son los dos que más se llaman síncronamente: Conway en acción.

Cuatro reglas de organización que la arquitectura necesita para sostenerse:

  1. Equipos alineados a flujo, dueños de una capacidad de negocio de extremo a extremo (código, datos, despliegue, guardia), en lugar de equipos por capa (frontend, backend, base de datos), que convierten cada funcionalidad en un proyecto de tres equipos.
  2. Un equipo de plataforma interna que ofrece Kubernetes, el mesh, Kafka, la observabilidad, el pipeline de despliegue y las librerías servicios/comun/ como un producto con usuarios (los equipos de producto), con documentación y SLO propios. Sin él, cada equipo reinventa la infraestructura, y la uniformidad que hace posible operar seis servicios se pierde. En Kilómetro Cero es el antiguo equipo de "datos" ampliado con las personas que montaron Kubernetes en 07-05.
  3. Contratos como acuerdos entre equipos: cambiar inventario.proto o el esquema de stock.actualizado es una negociación con los consumidores, verificada por las pruebas de contrato de 07-06, no una decisión unilateral.
  4. Guardia por servicio, no una guardia central: el equipo que despliega es el que recibe la alerta de su SLO (07-01, 07-03). Es lo que alinea los incentivos para hacer servicios operables.

  1. Cuándo no usar microservicios y el retorno al monolito modular

Kilómetro Cero necesitaba microservicios porque tenía cinco síntomas medibles y cuatro equipos. La mayoría de las aplicaciones no los tienen, y en ellas una arquitectura de microservicios es un impuesto sin contrapartida: cada frontera de red cuesta latencia, fallos parciales, consistencia eventual, contratos, despliegues y guardias.

Señal Qué indica Qué hacer en su lugar
Un solo equipo (menos de 8-10 personas) No hay conflicto de despliegue que resolver Monolito modular con paquetes por dominio y API interna (api.py) por paquete
El dominio aún no se entiende bien (producto nuevo, pivotes frecuentes) Las fronteras que se corten hoy serán las equivocadas mañana, y mover código entre servicios es caro Monolito modular; cortar cuando el lenguaje se estabilice
Todo el tráfico cabe en dos o tres réplicas del proceso No hay necesidad de escalado diferenciado Escalar el monolito horizontalmente; caché y réplicas de lectura
La mayoría de operaciones necesitan transacciones sobre varios "dominios" Cada frontera obligaría a una saga Mantener juntos los datos que cambian juntos
No hay capacidad de operar Kubernetes, Kafka, observabilidad distribuida La plataforma consumirá al equipo Un proceso, una base de datos, un despliegue sencillo; PaaS
Latencia extremadamente sensible (trading, juegos) Cada salto de red cuesta Módulos en proceso
El objetivo es "modernizar" sin síntoma concreto Complejidad sin justificación (error de 01-06: "elegir tecnologías antes que problemas") Escribir los síntomas; si no hay, no migrar

El retorno al monolito modular no es un fracaso, y hay casos públicos de empresas que han reagrupado servicios que se llamaban en cadena para todo. Las señales para reagrupar dos servicios son las inversas de las de cortar: cambian siempre juntos, todos sus cambios exigen desplegarlos a la vez, la traza de cualquier operación los atraviesa siempre a ambos, o los posee el mismo equipo y no tienen necesidades de escalado distintas. En Kilómetro Cero, si en dos años pagos no tuviera más lógica que el adaptador a la pasarela, sería candidato a volver dentro de pedidos como paquete con su api.py: la ACL y la idempotencia se conservarían; la frontera de red, no.

  1. Registrar las decisiones: el ADR-001

En 01-06 se dio el consejo de mantener un registro de decisiones de arquitectura. Un ADR (Architecture Decision Record) es un documento breve, numerado, inmutable una vez aceptado (si la decisión cambia, se escribe otro ADR que lo sustituye), con cuatro partes: contexto, decisión, alternativas consideradas y consecuencias. Su valor no está en la decisión, que suele conocerse, sino en el contexto y las consecuencias, que son lo que se olvida y lo que dentro de un año permitirá saber si la decisión sigue siendo válida. Este es el primero de Kilómetro Cero, que el proyecto final (08-05) completará hasta el ADR-006.

# ADR-001: Extraer `inventario` del monolito como servicio con base de datos propia

- **Estado**: Aceptado (fase 4 del plan de estrangulamiento)
- **Fecha**: 2026-03-10
- **Decisores**: equipo de pedidos y pagos, equipo de plataforma
- **Sustituye a**: —

## Contexto

El stock vive en la tabla `productos.unidades` del monolito y se descuenta dentro de la
transacción de confirmación de pedido, que abarca también el cobro en la pasarela externa.
Síntomas (01-06): (2) la lentitud de la pasarela mantiene bloqueos de stock durante 30 s y
agota las conexiones de PostgreSQL; (3) el equipo de pedidos no puede desplegar cambios en
las reglas de reserva sin arrastrar al catálogo y al reparto. El stock exige consistencia
fuerte por mercado (no vender la misma pieza dos veces), y solo `pedidos` lo modifica.
`catalogo` únicamente necesita saber si un producto está disponible, con retraso tolerable
de segundos.

## Decisión

Extraer `inventario` como bounded context propio, con base de datos `km0_inventario`
(PostgreSQL con Patroni, 03-04/07-03), API gRPC `contratos/inventario.proto`
(`ReservarStock`, `ConsultarStock`, `ObservarCambios`) y eventos `stock.actualizado`,
`stock.reservado`, `stock.liberado` publicados vía outbox. La reserva de stock pasa a ser
un paso de la saga de pedido (03-05), con caducidad de 15 minutos y compensación
`liberar_reserva`. Migración de datos con CDC (Debezium sobre el WAL del monolito) y
verificación de igualdad durante tres semanas antes de invertir la dirección.

## Alternativas consideradas

1. **Dejar el stock en `pedidos`** (misma base, sin frontera). Rechazada: acopla las
   reglas de reserva al ciclo de despliegue de pedidos y no permite a `catalogo` conocer
   la disponibilidad sin consultar la base de pedidos.
2. **Stock en Cassandra junto a los pedidos**. Rechazada: el descuento de stock necesita
   comparar-y-actualizar con consistencia fuerte por fila; Cassandra lo ofrece con
   transacciones ligeras (Paxos) a un coste alto y `inventario` no necesita su escala de
   escritura (03-02, 04-04).
3. **Transacción distribuida 2PC entre `pedidos` e `inventario`**. Rechazada: bloqueos
   mientras el coordinador decide, y la pasarela externa no participa en 2PC (03-05).

## Consecuencias

- (+) `pagos` lento ya no bloquea stock: la reserva tiene caducidad y la saga compensa.
- (+) Las reglas de reserva se despliegan de forma independiente.
- (+) `catalogo` obtiene la disponibilidad por eventos (vista `productos_vista`).
- (−) Consistencia eventual entre stock real y catálogo: ventana de segundos en la que
  un producto agotado aparece disponible; se acepta y se maneja con "sin stock" en la
  confirmación.
- (−) Una llamada síncrona más en el camino crítico de confirmar pedido (p99 +12 ms);
  el SLO de 500 ms sigue cumpliéndose.
- (−) Nuevo componente con guardia: Patroni, backups y PITR de `km0_inventario` (07-03).
- (−) Los informes que hacían `JOIN` de stock con pedidos se reescriben sobre el lago
  de eventos (fase 5).

Los ADR viven en el repositorio (km0/docs/adr/), se revisan en pull request como el código y se enlazan desde el ticket que originó el cambio. Un ADR que solo dice "usamos Kafka" sin contexto ni consecuencias negativas no sirve: las consecuencias negativas son la parte que más valor tiene.

Errores Comunes y Consejos

  • Cortar por tabla o por capa técnica. Produce servicios-entidad y cadenas síncronas. Corta por capacidad de negocio; si el servicio no tiene un verbo de negocio en su API, replantéalo.
  • Compartir la base de datos "solo para lecturas". Es la forma más silenciosa de crear un monolito distribuido: el primer ALTER TABLE rompe al vecino. Vistas materializadas por eventos o composición en la API; nunca un SELECT cruzado.
  • Convertir cada consulta cruzada en una llamada síncrona. La disponibilidad se multiplica y la latencia se suma. Si la consulta es frecuente y tolera segundos de retraso, es una proyección CQRS.
  • Versionar la API rompiendo la anterior sin periodo de coexistencia. Toda versión nueva convive con la antigua, marcada deprecated, con fecha de retirada y con una métrica que diga quién la sigue usando.
  • Migrar en "big bang". Estrangular por rutas, empezar por lecturas, migrar datos con CDC y verificación, y conservar siempre la vuelta atrás (un cambio de ruta en Kong).
  • Ignorar a Conway. Fronteras de servicio que no coinciden con equipos se acoplan o se pelean. Diseña la organización junto con la arquitectura.
  • Adoptar microservicios sin síntomas. Escribe los síntomas antes de cortar. Si no los hay, el monolito modular con api.py por paquete es la respuesta correcta, y es reversible.
  • Consejo: cada bounded context con un solo módulo público; una prueba de arquitectura (con import-linter o similar) que falle si alguien importa un módulo privado de otro contexto.
  • Consejo: escribe el ADR antes de la decisión, no después: obliga a enumerar alternativas y consecuencias negativas cuando aún se puede cambiar de idea.

Ejercicios

Ejercicio 1: cortar un dominio nuevo

Kilómetro Cero quiere añadir valoraciones: Ana puntúa de 1 a 5 cada producto que ha recibido y escribe un comentario; el productor responde; la ficha del catálogo muestra la media y los tres últimos comentarios; el panel del productor muestra sus valoraciones pendientes de respuesta. Solo se puede valorar un producto de un pedido entregado. Decide: (a) ¿es un bounded context propio o parte de catalogo o de pedidos? Justifícalo con las heurísticas del apartado 3. (b) ¿Qué comunicación tiene con los demás contextos (síncrona/asíncrona, y en qué dirección)? (c) ¿Cómo obtiene la ficha del catálogo la media de valoraciones sin llamar síncronamente al nuevo contexto?

Ejercicio 2: detectar el monolito distribuido

Un equipo propone esta implementación de "confirmar pedido": el BFF llama a pedidos, que llama a clientes (nuevo servicio que expone la tabla de usuarios) para validar la dirección, luego a precios (nuevo, calcula el importe con IVA) y a inventario, pagos y reparto en secuencia, todo síncrono; analitica consulta cada noche las tablas de pedidos con un usuario de solo lectura; y precios comparte con catalogo la librería km0-modelo con las clases Producto y Precio. Enumera cada violación de las cinco características del apartado 2, nómbrala (servicio-entidad, nanoservicio, monolito distribuido, base compartida...) y propón la corrección con las técnicas de la lección.

Ejercicio 3: escribir un ADR

Escribe el ADR-002, "Cassandra para pedidos", con las cuatro partes (contexto, decisión, alternativas rechazadas, consecuencias positivas y negativas), apoyándote en lo que se decidió en 03-02 y 04-04. Incluye al menos tres consecuencias negativas.

Soluciones

Ejercicio 1.

(a) Contexto propio, valoraciones. Lenguaje: "valoración", "media", "respuesta del productor" no existen en ningún otro contexto, y "producto" aquí es solo un identificador. Cohesión: las reglas (una valoración por línea entregada, moderación, respuesta única) cambian juntas y no con el catálogo. Tasa de cambio: es una funcionalidad nueva que evolucionará rápido (moderación, fotos, filtros), distinta del ritmo de pedidos. Datos que cambian juntos: la valoración no se escribe nunca en la misma transacción que el pedido ni que la ficha. Equipo: encaja en el de catálogo y productores, que puede poseer dos contextos. Meterlo en catalogo acoplaría la moderación de comentarios a los despliegues del catálogo en campaña; en pedidos contaminaría el contexto más crítico con lógica que no tiene que ver con la compra.

(b) Entrada: valoraciones consume pedido.entregado (evento de reparto, asíncrono) para saber qué líneas pueden valorarse: guarda (cliente, pedido, producto, entregado_en) en su propia base; así, al recibir "Ana valora queso-curado del P-2026-000124", comprueba localmente sin llamar a pedidos. Salida: publica valoracion.creada y valoracion.respondida en un tópico valoraciones.eventos. No hay ninguna llamada síncrona entre servicios: la única síncrona es del BFF/gateway a valoraciones para crear y listar.

(c) catalogo mantiene una proyección: consume valoracion.creada y actualiza en productos_vista (o una tabla productos_valoracion) suma_puntos, num_valoraciones y una lista de los tres últimos comentarios (JSON). La media se calcula al leer (suma/num) y tolera segundos de retraso. Con el consumidor idempotente, una valoración reprocesada no cuenta dos veces. Es exactamente la vista productos_vista del apartado 6 con otra fuente.

Ejercicio 2.

Elemento propuesto Violación Nombre Corrección
clientes expone la tabla de usuarios y pedidos lo llama para validar la dirección Servicio sin capacidad de negocio; llamada síncrona para un dato estable Servicio-entidad La dirección de entrega viaja en el comando de crear pedido (el cliente la eligió en pantalla); la identidad ya viene en el JWT (06-01). pedidos guarda una copia de la dirección en el pedido (desnormalización legítima: la dirección del pedido no cambia si Ana se muda)
precios calcula el importe con IVA en un servicio aparte Coste de red por una función pura Nanoservicio Función dentro de pedidos (la decisión de 01-06). Si las reglas de precio crecieran (tarifas por mercado, promociones complejas), sería un contexto precios con su propia lógica, pero consultado por evento o con precios publicados en el catálogo
Cadena síncrona inventario → pagos → reparto reparto no gobierna la confirmación; disponibilidad multiplicada Monolito distribuido (acoplamiento temporal) inventario y pagos síncronos dentro de la saga (03-05); reparto consume pago.confirmado de forma asíncrona y publica reparto.asignado
analitica con usuario de solo lectura sobre las tablas de pedidos Rompe "dueño de sus datos"; acopla esquemas; carga nocturna en producción (síntoma 5) Base de datos compartida Consumidor conformista de pedidos.eventos hacia el lago (05-05)
Librería km0-modelo compartida con clases de dominio Un cambio en Producto obliga a redesplegar ambos; niega el lenguaje ubicuo (mismo Producto para dos contextos) Monolito distribuido (acoplamiento por librería) Cada contexto con su propio modelo; lo compartido se limita a servicios/comun/ (métricas, logs, trazas, resiliencia: infraestructura, no dominio) y a los contratos (.proto, esquemas de eventos), que se versionan

Ejercicio 3. Un ADR-002 aceptable:

  • Contexto. Los pedidos crecen 1,2 M al mes en campaña; el patrón de acceso es "pedidos de un cliente por fecha" y "pedido por id", sin consultas ad hoc; se exige disponibilidad para aceptar pedidos incluso con un nodo caído o una partición (03-02: pedidos elige disponibilidad); PostgreSQL con Patroni tiene un primario único que limita la escritura y cuya conmutación (30 s) supondría rechazar pedidos en campaña.
  • Decisión. km0_pedidos en Cassandra, 3 nodos, RF=3, tablas pedidos_por_cliente y pedidos_por_id (desnormalizadas por consulta), escrituras con QUORUM y lecturas con LOCAL_QUORUM para el estado del pedido y ONE para listados; tabla sagas en el mismo keyspace.
  • Alternativas rechazadas. PostgreSQL particionado por cliente con Citus (mantiene SQL pero introduce un coordinador y transacciones distribuidas que el patrón de acceso no necesita); MongoDB (documentos encajan, pero el modelo de réplica con primario único tiene el mismo problema de conmutación); mantener PostgreSQL y aceptar los 30 s de indisponibilidad (rechazada por el síntoma 1: los picos coinciden con cuando más duele).
  • Consecuencias. (+) Escritura lineal con nodos; sin conmutación de primario; tolera un nodo caído sin rechazar pedidos. (−) Sin JOIN ni consultas ad hoc: cada pregunta nueva exige una tabla nueva o una vista CQRS (el panel del productor del apartado 6). (−) Consistencia ajustable que el código debe elegir en cada operación: un error de nivel es un bug silencioso. (−) Nueva tecnología que operar: compactaciones, reparaciones, snapshots (07-03), formación del equipo. (−) Las transacciones ligeras (Paxos) son caras: por eso el stock no va aquí (ADR-001).

Conclusión

Una arquitectura de microservicios es un sistema, y no una colección de servicios, cuando cada servicio cumple cinco propiedades: representa una capacidad de negocio, es dueño de sus datos, se despliega solo, lo posee un equipo y sus fallos no se propagan. Las fronteras se trazan donde el lenguaje cambia de significado (bounded contexts), guiadas por la cohesión, la tasa de cambio, los datos que cambian juntos y los equipos, y el mapa de contextos de Kilómetro Cero muestra el resultado: solo dos flechas síncronas (pedidos → inventario, pedidos → pagos) y todo lo demás por eventos. Renunciar al JOIN obliga a componer en la API, a mantener vistas materializadas por eventos o a separar formalmente escritura y lectura con CQRS, como el panel de la Quesería Montblanc y la tabla productos_vista, con la consistencia eventual como estado normal y no como defecto. La comunicación se elige caso por caso (síncrona solo cuando la respuesta gobierna la siguiente decisión), las APIs públicas se versionan con coexistencia y métricas de uso, el gateway y el BFF separan lo transversal de lo específico de cada cliente, y el mesh compensa cuando la heterogeneidad o la exigencia de mTLS obligatorio superan lo que una librería común puede dar. El plan de estrangulamiento de 01-06 se ha revisado fase a fase (strangler fig, ACL, branch by abstraction, CDC), la ley de Conway explica por qué la organización y la arquitectura deben diseñarse juntas, la tabla de señales dice cuándo el monolito modular es la respuesta correcta, y el ADR-001 deja escrito por qué inventario salió del monolito y qué se paga por ello.

Queda una pieza de la plataforma que este mapa solo ha nombrado: la flecha discontinua reparto.posiciones termina en Kafka, pero Ana no lee Kafka. Entre el tópico y su pantalla hay un último tramo con reglas propias (navegadores, móviles, dispositivos en redes malas, miles de conexiones abiertas), y ese tramo, con MQTT y WebSockets que 02-01 dejó presentados, es la siguiente lección: Sistemas de Mensajería en Tiempo Real.

Curso de Arquitecturas Distribuidas

Módulo 1: Introducción a los Sistemas Distribuidos

Módulo 2: Comunicación en Sistemas Distribuidos

Módulo 3: Consistencia y Replicación

Módulo 4: Almacenamiento Distribuido

Módulo 5: Computación Distribuida

Módulo 6: Seguridad en Sistemas Distribuidos

Módulo 7: Monitoreo y Mantenimiento

Módulo 8: Casos de Estudio y Aplicaciones

© Copyright 2026. Todos los derechos reservados