En 01-06 abrimos un monolito Python con una base de datos PostgreSQL, un despliegue de martes y jueves y cinco síntomas que lo empujaban a cambiar, y dibujamos una arquitectura objetivo con una tabla que remitía cada componente a una lección futura. Treinta y ocho lecciones después, todas esas casillas están construidas: gRPC y Kafka, sagas y réplicas, Cassandra y Redis, Spark y Flink, Keycloak y Vault, Prometheus y Kubernetes, MQTT y WebSockets, Terraform y Lambda, la CDN y la cola SQLite de la furgoneta. Esta última lección no añade ninguna pieza nueva: junta las que hay. Primero la arquitectura completa en un solo diagrama y la tabla de 01-06 cerrada; después el recorrido de un pedido de Ana desde su navegador hasta la factura en S3, con todo lo que deja a su paso en trazas, métricas y auditoría; luego las seis decisiones de arquitectura como ADR, con sus alternativas rechazadas y sus consecuencias; los cinco síntomas de 01-06 con su resolución; la evaluación de la plataforma con los criterios del propio curso (falacias, consistencia por dato, SLO, RPO/RTO, controles de seguridad); lo que queda fuera; el proyecto que el alumno puede construir en su portátil con hitos verificables; y una guía para seguir profundizando. Los ejercicios son de integración: incorporar un dominio nuevo, analizar un incidente de extremo a extremo y recortar la arquitectura para tres personas. La conclusión cierra el curso.

Contenido

  1. La arquitectura final de Kilómetro Cero
  2. La tabla de 01-06, cerrada
  3. El recorrido de un pedido de Ana de extremo a extremo
  4. Lo que el pedido deja a su paso: trazas, métricas y auditoría
  5. Las decisiones de arquitectura como ADR
  6. Los cinco síntomas de 01-06, revisitados
  7. Evaluación con los criterios del curso
  8. Lo que queda fuera y siguientes pasos
  9. El proyecto: Kilómetro Cero reducido en docker-compose
  10. Guía de estudio
  11. Errores Comunes y Consejos
  12. Ejercicios
  13. Conclusión

  1. La arquitectura final de Kilómetro Cero

flowchart TB
    subgraph Clientes
        ANA[Navegador / móvil de Ana]
        APPR[App repartidores]
        PANP[Panel productores]
        FURG[furgoneta-3<br/>agregador SQLite]
    end

    subgraph Borde[Borde de red]
        CDN[CDN + Worker<br/>caché por mercado, JWT]
    end

    subgraph Region[Región eu-west-1, 3 AZ]
        ALB[ALB] --> KONG[Kong<br/>jwt, rate-limiting, X-Request-Id]
        KONG --> BFF[BFF app repartidores]
        subgraph EKS[EKS + service mesh mTLS]
            CAT[catalogo]
            PED[pedidos<br/>saga, outbox]
            INV[inventario<br/>gRPC]
            PAG[pagos<br/>ACL pasarela]
            REP[reparto<br/>ws_servidor, puente MQTT]
            ANA2[analitica]
            MQ[Mosquitto]
            OBS[Prometheus · Loki · Tempo · Grafana]
            KC[Keycloak realm km0]
            VLT[Vault / Secrets Manager]
        end
        KONG --> CAT & PED & REP
        BFF --> REP & PED & CAT
        PED -- gRPC mTLS --> INV
        PED -- gRPC mTLS --> PAG
        PAG --> PAS[(Pasarela de pagos)]
        subgraph Datos[Datos]
            RDS[(RDS PostgreSQL Multi-AZ<br/>km0_inventario)]
            CAS[(Cassandra 3 nodos<br/>km0_pedidos)]
            RED[(ElastiCache Redis<br/>caché, limitador, pub/sub)]
            S3[(S3 km0-fotos, facturas,<br/>backups, auditoria)]
            LAGO[(Lago S3 /km0/eventos)]
            ANADB[(PostgreSQL km0_analitica)]
        end
        INV --> RDS
        PED --> CAS
        CAT --> RED
        CAT --> S3
        subgraph Mensajeria[Mensajería]
            MSK[(MSK Kafka<br/>pedidos.eventos, inventario.alertas,<br/>reparto.posiciones, reparto.panel,<br/>auditoria.eventos)]
        end
        PED -. outbox .-> MSK
        INV -. outbox .-> MSK
        MSK -.-> CAT & REP & ANA2
        MQ --> REP --> MSK
        subgraph Computo[Computación]
            FLK[Flink alertas_stock, panel]
            SPK[Spark ventas_diarias, EMR spot]
            AIR[Airflow km0_ventas_diarias]
        end
        MSK --> FLK --> MSK
        MSK --> LAGO --> SPK --> ANADB
        AIR --> SPK
        subgraph Serverless
            LMIN[Lambda miniaturas]
            LFAC[Lambda facturas]
        end
        S3 -- s3:ObjectCreated --> LMIN --> S3
        MSK -- pago.confirmado --> LFAC --> S3
    end

    subgraph DR[Región eu-central-1, pilot light]
        RDSR[(Réplica RDS)]
        S3R[(S3 replicado)]
        MSKR[(MSK mínimo, MirrorMaker)]
    end
    RDS -.-> RDSR
    S3 -.-> S3R
    MSK -.-> MSKR

    ANA --> CDN --> ALB
    PANP --> CDN
    APPR --> ALB
    FURG -- MQTT QoS 1 --> MQ
    REP -- WebSocket --> ANA
    KC -. OIDC .- ANA & APPR & PANP

El diagrama tiene una pieza que hasta ahora solo estaba prometida: el BFF de la app de repartidores, anunciado en 06-05 y situado en 08-01. Es un servicio pequeño, propiedad del equipo de reparto, detrás de Kong, que compone la pantalla de inicio de la app (ruta del día de reparto, estado de los pedidos de esa ruta de pedidos, nombres y fotos de los productos de catalogo) en una sola respuesta, con llamadas en paralelo, timeouts cortos y degradación (si catalogo no responde, la ruta se muestra sin fotos). No tiene base de datos: es composición en la API (08-01, apartado 5), justificada porque la app hace pocas llamadas, los datos son pequeños y necesita frescura.

  1. La tabla de 01-06, cerrada

La tabla de 01-06 era una promesa: cada componente, la lección donde se construiría. Esta es la misma tabla, cerrada, con lo que finalmente se construyó y el fichero o el nombre que lo materializa en km0/.

Componente Lección Qué se construyó
Protocolos: TCP/UDP/HTTP; MQTT y WebSockets presentados 02-01 Cliente TCP con timeout; medición de cabeceras
RPC síncrono entre servicios 02-02 Stubs, marshalling, semánticas de fallo
gRPC con contratos 02-03 contratos/inventario.proto (ReservarStock, ConsultarStock, ObservarCambios)
Bus de eventos y colas 02-04 Kafka pedidos.eventos (6 particiones, clave = pedido), envoltura id_evento/tipo/version/fecha_ms/origen/datos
Idempotencia, outbox, DLQ, consumidores en competencia 02-05 Tabla outbox, mensajes_procesados, tópicos .retry y DLQ
Modelos de consistencia; CRDT 03-01 Tabla de consistencia por dato; G-Counter
CAP y PACELC: inventario (C) y pedidos (A) 03-02 Decisión por dominio
Consenso: Raft, etcd, elección de líder 03-03 etcd para Patroni; controlador de Kafka
Replicación: primario/réplica, multilíder, quórums 03-04 inv-bcn/inv-vlc; Patroni
Sagas 03-05 saga_pedido.py, tabla sagas, orquestación y coreografía
Particionado y hashing consistente 04-01 Anillo, particiones de Kafka y Cassandra
Sistemas de ficheros distribuidos 04-02 Lago /km0/eventos/... (HDFS, luego S3)
Almacenamiento de objetos 04-03 MinIO/S3 km0-fotos, km0-facturas, km0-backups, km0-auditoria; fotos.py; URLs prefirmadas
Bases de datos distribuidas 04-04 Cassandra km0_pedidos (pedidos_por_cliente, pedidos_por_id), repositorio con niveles de consistencia
Cachés 04-05 Redis cache-aside para catalogo, invalidación por stock.actualizado, Redis Cluster
Modelos de computación 05-01 BSP, dataflow, actores
MapReduce y Hadoop 05-02 Ventas por mercado sobre el lago
Spark 05-03 ventas_diarias.py
Flujos 05-04 Flink alertas_stock.py (inventario.alertas), panel reparto.posicionesreparto.panel
Pipelines 05-05 Airflow DAG km0_ventas_diarias
AuthN/AuthZ 06-01 JWT (sub, roles, jti), RBAC/ABAC en servicios
Cifrado 06-02 TLS, cifrado en reposo con claves propias, cifrado de campo para pagos y posiciones
Identidades 06-03 Keycloak realm km0, clientes web-km0, app-repartidores, pedidos-servicio; OIDC
mTLS y secretos 06-04 km0-ca, AuthInterceptor/ServicioInterceptor, Vault (AppRole, credenciales dinámicas)
Gateway y auditoría 06-05 Kong (/api/v1/catalogo, /api/v1/pedidos, jwt, rate-limiting, X-Request-Id), auditoria.eventos, km0-auditoria
Métricas y SLO 07-01 servicios/comun/metricas.py, km0_pedidos_total, SLO 99,9 % y p99 < 500 ms, alertas por tasa de consumo de presupuesto de error
Logs y trazas 07-02 servicios/comun/{logs,trazas}.py, Loki, Tempo, OpenTelemetry, X-Request-Id
Fallos y recuperación 07-03 Patroni + etcd, ISR, checkpoints, backups y PITR en km0-backups, RPO/RTO, DR pilot light, runbooks
Resiliencia 07-04 servicios/comun/resiliencia.py: timeouts, reintentos con jitter, circuit breaker, bulkhead, load shedding
Automatización y orquestación 07-05 Ansible, k8s/pedidos-deployment.yaml, HPA, canary, GitOps, mesh
Pruebas y caos 07-06 Testcontainers, contratos, k6 tests/carga/vendimia.js, Toxiproxy, Chaos Mesh
Microservicios 08-01 Bounded contexts, servicios/inventario/api.py, productos_vista (CQRS), versionado /v1/v2, ADR-001
Tiempo real 08-02 Mosquitto con ACL, furgoneta_mqtt.py, puente_mqtt_kafka.py, ws_servidor.py, seguimiento.js, SSE de alertas
Nube 08-03 infra/aws/main.tf (VPC 3 AZ, EKS, RDS Multi-AZ, MSK, ElastiCache, S3, IRSA), FinOps
Serverless y edge 08-04 serverless/miniaturas, serverless/facturas, template.yaml, saga en Step Functions, Worker de CDN, borde/furgoneta/agregador.py
Extremo a extremo 08-05 Esta lección

  1. El recorrido de un pedido de Ana de extremo a extremo

Ana, en Valencia, compra dos queso-curado de la Quesería Montblanc y un vino-crianza de la Bodega Roble Alto. El pedido es P-2026-000125. Este es el recorrido completo, con la lección en la que se construyó cada paso.

sequenceDiagram
    autonumber
    participant A as Navegador de Ana
    participant CDN as CDN + Worker
    participant K as Kong
    participant P as pedidos
    participant I as inventario
    participant PG as pagos
    participant KF as Kafka
    participant C as catalogo
    participant AN as analitica (Flink, lago)
    participant R as reparto
    participant L as Lambda facturas

    A->>CDN: GET /api/v1/catalogo/productos/queso-curado (JWT)
    CDN-->>A: HIT: ficha por mercado valencia (08-04)
    A->>CDN: POST /api/v1/pedidos (JWT, Idempotency-Key)
    CDN->>K: verifica firma JWT, reenvía (no cacheable)
    K->>K: plugin jwt, rate-limiting, X-Request-Id (06-05)
    K->>P: POST /pedidos + X-Request-Id
    P->>P: Idempotency-Key ya vista? no → crear pedido (creado) en Cassandra (04-04)
    P->>P: iniciar saga: fila en sagas (03-05)
    P->>I: gRPC ReservarStock (mTLS, ServicioInterceptor, timeout 300 ms, circuit breaker)
    I->>I: SELECT FOR UPDATE en RDS, reserva 15 min; outbox: stock.reservado, stock.actualizado
    I-->>P: Reserva OK
    P->>PG: gRPC Cobrar (Idempotency-Key cobro-P-2026-000125)
    PG->>PG: pasarela externa (ACL), 1,2 s
    PG-->>P: pago autorizado
    P->>P: estado pagado; outbox: pedido.creado, pago.confirmado (misma transacción)
    P-->>K: 201 Created {id, estado: pagado}
    K-->>CDN: 201 Created
    CDN-->>A: 201 (p99 < 500 ms sin contar la pasarela: SLO 07-01)
    Note over P,KF: relay de outbox → pedidos.eventos (partición por id de pedido)
    KF-->>C: stock.actualizado → productos_vista + invalidar Redis (08-01, 04-05)
    KF-->>AN: pedido.creado, pago.confirmado → lago /km0/eventos; Flink alertas_stock (05-04)
    KF-->>R: pago.confirmado → asignar furgoneta-3, publicar reparto.asignado
    KF-->>L: pago.confirmado (grupo facturas-lambda) → PDF en km0-facturas (08-04)
    A->>R: wss://.../ws {jwt}, {suscribir: P-2026-000125}
    R->>R: autorizar sub = cliente; tema furgoneta:furgoneta-3
    loop cada 5 s
        Note over R: furgoneta-3 → MQTT → puente → reparto.posiciones → Redis pub/sub
        R-->>A: {tipo: posicion, seq, lat, lon}
    end

Cinco observaciones sobre el diagrama que resumen el curso:

  1. Solo dos llamadas síncronas entre servicios (pasos 9 y 12), las dos con timeout, circuit breaker y mTLS, y las dos porque su respuesta gobierna la siguiente decisión (08-01). Todo lo demás son eventos.
  2. Ninguna transacción cruza servicios. El pedido se crea en Cassandra, la reserva en RDS y el cobro en la pasarela como transacciones locales; la saga las encadena y compensa (03-05); la outbox garantiza que los eventos salen si y solo si la transacción local se confirma (02-05).
  3. La idempotencia aparece cuatro veces: la Idempotency-Key de Ana en Kong/pedidos (un doble clic no crea dos pedidos), el id_reserva en inventario, la clave cobro-<pedido> hacia la pasarela, y el id_evento en cada consumidor. Un reintento en cualquier punto es seguro.
  4. Ana recibe la respuesta en el paso 17, antes de que exista la vista del catálogo, la asignación de reparto o la factura. Lo que ve después (posición de la furgoneta, factura en su área personal) llega por caminos asíncronos con segundos de retraso, que es el estado normal (08-01).
  5. El borde y el gateway hacen su trabajo antes de que nada llegue a los servicios: el JWT se verifica tres veces (Worker, Kong, servicio), el rate limiting protege a pedidos, y la ficha del producto ni siquiera llegó a la región.

  1. Lo que el pedido deja a su paso: trazas, métricas y auditoría

Un pedido no solo produce estado; produce evidencia. Lo que queda tras P-2026-000125:

Señal Dónde Contenido Lección
Traza Tempo Un trace_id generado en Kong (o propagado desde el navegador), con spans: kongpedidos POST /pedidosinventario.ReservarStock (12 ms) → pagos.Cobrar (1 210 ms, con span hijo pasarela) → cassandra.insertoutbox.insert. Después, spans enlazados (no hijos) en los consumidores: catalogo.proyeccion, reparto.asignar, facturas.handler, unidos por el id_evento propagado en las cabeceras de Kafka 07-02
Logs Loki Líneas JSON con request_id, trace_id, pedido_id, sub=u-ana, nivel y mensaje, en cada servicio; consultables con {servicio="pedidos"} | json | pedido_id="P-2026-000125" 07-02
Métricas Prometheus km0_pedidos_total{estado="confirmado"} +1; histograma de latencia de POST /pedidos (bucket 0,5 s); km0_grpc_client_duration_seconds{metodo="ReservarStock"}; km0_circuit_estado{dependencia="pagos"}=0; lag del grupo catalogo-proyeccion; km0_ws_conexiones +1 07-01
Auditoría auditoria.eventoskm0-auditoria (inmutable, con retención legal) {"sub": "u-ana", "accion": "pedidos.crear", "recurso": "P-2026-000125", "desde": ..., "request_id": ..., "resultado": "ok"}; y el acceso de pedidos-servicio a inventario registrado por el ServicioInterceptor 06-05
Eventos de negocio pedidos.eventos (retención 7 días) y lago /km0/eventos/pedidos.eventos/fecha=2026-09-15/ (para siempre) pedido.creado, stock.reservado, stock.actualizado, pago.confirmado, reparto.asignado 02-04, 04-02
Estado Cassandra (pedidos_por_cliente, pedidos_por_id, sagas), RDS (referencias, reservas, outbox), productos_vista, S3 (facturas/u-ana/P-2026-000125.pdf) El pedido, la reserva, la saga completada, la vista y la factura Módulos 3 y 4
Coste Factura del proveedor, por etiqueta Unas fracciones de céntimo: invocaciones, transiciones, GB-s, egress de la factura 08-03

La prueba de que la observabilidad está bien hecha es que con el pedido_id se puede reconstruir todo: de los logs al trace_id, de la traza a cada servicio y su latencia, de la métrica a si ese pedido fue uno de los que rompieron el SLO, del evento en el lago a lo que analitica calculó, de la auditoría a quién lo hizo. Es lo que el ejercicio 2 pone a prueba.

  1. Las decisiones de arquitectura como ADR

En 08-01 se escribió el ADR-001 completo. Estas son las seis decisiones estructurales de Kilómetro Cero en formato resumido: cada una con la alternativa que se rechazó y la consecuencia negativa que se aceptó, que es la parte que dentro de un año permitirá saber si sigue siendo válida.

ADR Decisión Alternativa rechazada y por qué Consecuencia aceptada Lección
ADR-001 Extraer inventario del monolito como bounded context con km0_inventario (PostgreSQL con Patroni / RDS Multi-AZ) y API gRPC + eventos Mantener el stock dentro de pedidos (acoplaría reglas y despliegues); stock en Cassandra (transacciones ligeras caras para comparar-y-actualizar); 2PC (bloqueos y pasarela externa fuera del protocolo) Consistencia eventual entre stock real y catálogo (ventana de segundos); una llamada síncrona más en el camino crítico (+12 ms p99); un componente con estado que operar 08-01
ADR-002 Cassandra (3 nodos, RF=3, QUORUM) para km0_pedidos, con tablas por consulta PostgreSQL con Citus (coordinador y transacciones distribuidas que el patrón de acceso no necesita); MongoDB (primario único con conmutación); PostgreSQL y aceptar 30 s de indisponibilidad en campaña Sin JOIN ni consultas ad hoc: toda pregunta nueva es una tabla o una vista CQRS; nivel de consistencia elegido en cada operación (un bug silencioso si se elige mal); operación de compactaciones, reparaciones y snapshots 03-02, 04-04
ADR-003 Sagas orquestadas desde pedidos (saga_pedido.py, tabla sagas) para confirmar pedido Coreografía pura (cada servicio reacciona a eventos: más desacoplada, pero el flujo queda implícito y difícil de seguir y de compensar en orden); Step Functions (latencia y coste por transición en un camino crítico de 1,2 M/mes; lock-in) pedidos conoce a inventario y a pagos (acoplamiento aceptado); el orquestador es un componente con estado que hay que recuperar al arrancar; ausencia de aislamiento entre sagas (contramedidas de 03-05: reservas con caducidad, estados pendientes) 03-05, 08-04
ADR-004 Kafka como bus de eventos único (pedidos.eventos con clave = pedido, reparto.*, inventario.alertas, auditoria.eventos), con outbox en los productores y consumidores idempotentes RabbitMQ para todo (sin retención ni replay, imprescindibles para reconstruir vistas CQRS y alimentar el lago); llamadas HTTP entre servicios para notificar (acoplamiento temporal, síntoma 2); un ESB con transformaciones (la lección de la SOA) Consistencia eventual como norma; orden solo por partición; esquemas de eventos como contratos que versionar; un clúster crítico (MSK) del que depende todo; retención finita salvo en el lago 02-04, 02-05, 08-01
ADR-005 Kubernetes (EKS) con service mesh, GitOps y canary como plataforma de despliegue de los servicios; servicios gestionados para datos (RDS, MSK, ElastiCache, S3) Máquinas virtuales con Ansible para todo (sin autoescalado ni reconciliación, despliegues manuales, síntoma 3); PaaS tipo Cloud Run/App Runner por servicio (menos control de red, mesh y operadores; encajaría para una versión pequeña); Kubernetes autogestionado (operar el plano de control sin beneficio) Complejidad de operación (namespaces, RBAC, mesh, operadores) asumida por un equipo de plataforma; 1-2 ms por salto del sidecar; lock-in selectivo en los gestionados; coste base permanente (≈ 3 900 €/mes con reservas) 07-05, 08-03
ADR-006 Serverless (Lambda con SAM) para tareas de evento esporádicas: miniaturas, facturas, webhooks de la pasarela, tareas programadas; Step Functions solo para flujos largos y raros (baja de productor) Consumidores en Kubernetes para todo (coste permanente para tareas que corren segundos al día; sondas y guardia sin motivo); serverless para pedidos o el WebSocket (latencia, estado, conexiones); Step Functions para la saga de pedido (ADR-003) Lock-in alto en los adaptadores (limitado separando manejador y lógica); reintentos automáticos que obligan a idempotencia estricta; arranques en frío; observabilidad que hay que unir a la del clúster; revisar el coste si la frecuencia se multiplica por diez 08-04

Cada ADR está en km0/docs/adr/ADR-00N-*.md, con el formato completo de 08-01. Nótese que tres de las seis decisiones (002, 003, 006) rechazan explícitamente una opción "más moderna" o "más gestionada": la arquitectura no es la suma de las mejores tecnologías sino de las decisiones que responden a un síntoma con un coste conocido.

  1. Los cinco síntomas de 01-06, revisitados

Síntoma (01-06) Qué pedía Cómo quedó resuelto Cómo se verifica
1. Los picos de campaña tumban todo (Vendimia ×15; 92 % lecturas de catálogo) Escalar el catálogo aparte, servir lecturas desde caché y fotos desde otro sitio, aislar fallos catalogo como servicio propio con Redis cache-aside (04-05), fotos en S3 con miniaturas por Lambda (04-03, 08-04) y CDN con immutable (08-04); HPA y nodos spot en campaña (07-05, 08-03); rate limiting en Kong (06-05); load shedding (07-04) k6 tests/carga/vendimia.js a ×15 en staging antes de cada campaña (07-06): p99 de catálogo < 100 ms, hit ratio de CDN > 95 %, pedidos sin degradación
2. Un fallo de pagos tumba todo (pasarela a 30 s; conexiones agotadas) pagos aislado con sus recursos, lentitud contenida, confirmación sin transacción global pagos con ACL frente a la pasarela (08-01); timeout, circuit breaker y bulkhead en pedidospagos (07-04); saga con reserva de stock con caducidad y estado "pago pendiente" (03-05); base de datos propia (ADR-001/002) Experimento de caos: Toxiproxy añade 30 s de latencia a la pasarela (07-06); el circuito se abre en < 10 s, el catálogo y el seguimiento siguen, los pedidos quedan pendientes y se completan al cerrar el circuito
3. Los equipos se pisan al desplegar (4 equipos, despliegue de martes y jueves) Unidades de despliegue independientes por dominio, sin parada Un Deployment por servicio con rolling y canary automático contra SLO (07-05); pruebas de contrato que protegen a los consumidores (07-06); GitOps: desplegar es hacer merge; equipos dueños de servicio y guardia por servicio (08-01) Frecuencia de despliegue por servicio (métrica DORA): reparto varias veces al día, pagos una vez por sprint, sin coordinación; cero despliegues coordinados en el último trimestre
4. La telemetría de repartidores en tiempo real (140 repartidores, 2,4 M posiciones/día en la tabla de pedidos; clientes preguntando) Canal para flujos de alta frecuencia, comunicación bidireccional, almacenamiento de series temporales MQTT con QoS 1, ACL y sesiones (08-02); puente a reparto.posiciones con clave (02-04); Cassandra para posiciones (04-04); Flink para el panel (05-04); WebSockets con fan-out por Redis a Ana y a los operadores (08-02); agregador en la furgoneta (08-04) Latencia de extremo a extremo (furgoneta → navegador) p95 < 3 s; ninguna escritura de posiciones en la base de pedidos; km0_ws_descartes_total ≈ 0 fuera de incidentes
5. La analítica ahoga la producción (3 h de consultas nocturnas sobre producción) Separar la carga analítica con una copia alimentada por eventos analitica consume pedidos.eventos y reparto.* hacia el lago (04-02); Spark ventas_diarias.py en EMR efímero con spot (05-03, 08-03); Flink para lo que no espera al lote (05-04); Airflow km0_ventas_diarias (05-05); km0_analitica separada Cero consultas de analitica contra km0_inventario o km0_pedidos (política de red y credenciales lo impiden: 06-04, 07-05); el DAG termina antes de las 06:00 con SLA en Airflow; los productores tienen previsiones que antes no se atrevían a pedir

  1. Evaluación con los criterios del curso

Un buen curso deja criterios, no solo técnicas. Estos son los cinco con los que Kilómetro Cero se evalúa a sí misma, y con los que el alumno puede evaluar cualquier otra plataforma.

Las ocho falacias (01-04)

Falacia Dónde la plataforma la asume como falsa
La red es fiable Timeouts en toda llamada (07-04); reintentos con idempotencia (02-05); QoS 1 y cola persistente en la furgoneta (08-02, 08-04); replicación y quórums (03-04)
La latencia es cero Dos llamadas síncronas como máximo en el camino crítico (08-01); caché en tres capas (04-05); CDN y Worker (08-04); SLO de latencia medido (07-01)
El ancho de banda es infinito gRPC binario (02-03); MQTT con 2 bytes de cabecera (08-02); coalescencia hacia clientes lentos (08-02); agregación de tramos en la furgoneta y CDN para el egress (08-04)
La red es segura TLS en todo (06-02); mTLS con mesh (06-04); JWT verificado en borde, gateway y servicio (06-01, 06-05, 08-04); ACL en el broker; grupos de seguridad y subredes privadas (08-03)
La topología no cambia Descubrimiento de servicios por Kubernetes y mesh (07-05); hashing consistente (04-01); reconexión con backoff en clientes (08-02); failover de RDS por DNS (08-03)
Hay un solo administrador Equipos dueños de servicio, plataforma interna, RBAC de Kubernetes e IAM por service account (08-01, 07-05, 08-03); contratos y ADR como acuerdos
El coste de transporte es cero Egress, tráfico entre AZ y NAT en el presupuesto; endpoints de VPC; client.rack; FinOps (08-03)
La red es homogénea Contratos protobuf y esquemas de eventos versionados (02-03, 02-05); el último tramo diseñado para redes móviles (08-02)

Modelo de consistencia por dato (03-01, 03-02)

Dato Modelo Mecanismo Quién lo lee así
Unidades disponibles de una referencia por mercado Fuerte (linealizable por fila) SELECT FOR UPDATE en el primario de RDS; réplica síncrona inventario.ReservarStock
Estado del cobro Fuerte en pagos; idempotente hacia la pasarela Transacción local + Idempotency-Key La saga
Pedido de Ana (por id) Quórum (lectura de tu propia escritura garantizada por QUORUM+QUORUM) Cassandra RF=3 pedidos al confirmar; Ana al consultar su pedido
Listado de pedidos de un cliente Eventual (ONE) Cassandra La pantalla "mis pedidos"
Disponibilidad en la ficha del catálogo Eventual (segundos) productos_vista por eventos + Redis + CDN 60 s Ana navegando
Panel del productor Eventual (segundos) Proyección CQRS Marta Puig
Posición de la furgoneta Eventual, "último gana" por seq MQTT retained, Redis ultima:, deduplicación por secuencia Ana, operadores
Stock físico del puesto del mercado Multilíder con autoridad local; CRDT para contadores Reconciliación al sincronizar El terminal de Lleida
Informes de ventas Eventual (horas) Lago + Spark nocturno Productores, dirección
Auditoría Fuerte en escritura, inmutable Outbox → auditoria.eventos → S3 con bloqueo de objetos Cumplimiento

La lección de esta tabla es que "fuerte" aparece dos veces y "eventual" seis: la consistencia fuerte se paga y se reserva para lo que de verdad la necesita.

SLO (07-01) y RPO/RTO (07-03)

Servicio / dato SLO RPO RTO
pedidos (crear) 99,9 % disponibilidad; p99 < 500 ms (sin la pasarela) km0_pedidos: 0 con QUORUM ante un nodo; minutos ante pérdida de región (snapshots) Nodo: 0 (sin failover); región: 1-2 h (runbook pilot light)
inventario 99,95 %; p99 < 100 ms en ReservarStock 0 (réplica síncrona Multi-AZ); minutos entre regiones ~60 s (failover RDS); región: 30-60 min
catalogo 99,9 %; p99 < 200 ms (100 ms con CDN) N/A (reconstruible desde eventos y S3) Minutos (releer pedidos.eventos)
reparto tiempo real 99 %; latencia de extremo a extremo p95 < 3 s Posiciones: se toleran pérdidas Minutos
Kafka 99,95 % 0 con acks=all, min.insync.replicas=2 Segundos (elección de líder)
analitica Informe diario antes de las 06:00 (SLA de Airflow) Horas Horas (rejecutar el DAG)
Fotos y facturas 99,99 % (S3) ~0 (versionado + CRR) Minutos (cambiar de región en la CDN)

Controles de seguridad (Módulo 6): quién puede qué

Actor Identidad Puede No puede Control
Ana (cliente) JWT de Keycloak, roles: [cliente], cliente web-km0 Ver el catálogo, crear pedidos propios, ver sus pedidos y facturas, seguir sus repartos por WebSocket, chatear con sus productores Ver pedidos de Marc; suscribirse a un mercado entero; publicar productos RBAC en servicios (06-01); autorización por suscripción (08-02); rate limit por consumidor (06-05)
Marta Puig (productora) JWT, roles: [productor], claim productor: queseria-montblanc Publicar y editar sus productos, subir fotos por URL prefirmada, ver su panel de pedidos, recibir alertas de stock por SSE, responder al chat Tocar productos de Huerta La Vega; ver datos personales de clientes más allá del nombre y la entrega ABAC "sus productos" en catalogo (06-01); URL prefirmada con prefijo (04-03); proyección filtrada por productor (08-01)
Jordi Sala (operador) JWT, roles: [operador] Panel de reparto de todos los mercados, reasignar furgonetas, enviar comandos por MQTT (vía reparto), ver auditoría de reparto Modificar precios; acceder a datos de pago RBAC; reparto publica en comandos con la identidad puente (08-02); auditoría de cada comando
furgoneta-3 (dispositivo) Usuario MQTT propio, credencial en Vault, revocable Publicar en km0/reparto/furgoneta-3/{posicion,estado,tramo}, leer sus comandos Publicar como furgoneta-7; leer nada más ACL de Mosquitto (08-02); validación en el puente; cifrado local (08-04)
pedidos (servicio) Certificado mTLS emitido por el mesh (SPIFFE), cliente pedidos-servicio en Keycloak, service account con rol IAM ReservarStock, ConsultarStock en inventario; Cobrar en pagos; producir en pedidos.eventos; leer sus secretos Leer km0_inventario directamente; producir en reparto.*; acceder a S3 ServicioInterceptor por método (06-04); políticas de autorización del mesh y de red (07-05); IRSA (08-03); credenciales dinámicas de Vault
Lambda facturas Rol IAM de la función Consumir pedidos.eventos (grupo propio), escribir en km0-facturas, PutItem en km0-idempotencia Leer km0-fotos; escribir en Kafka Políticas de SAM (08-04)
Worker de CDN Ninguna credencial secreta Verificar firmas con la clave pública, cachear respuestas public Acceder a bases de datos; ver la clave privada Diseño "nada secreto en el borde" (08-04)
Equipo de plataforma IAM con MFA, RBAC de Kubernetes por namespace Operar la infraestructura, aplicar Terraform desde el pipeline Leer datos de pago descifrados; aplicar Terraform desde un portátil en producción Mínimo privilegio, GitOps, auditoría de CloudTrail (08-03)

  1. Lo que queda fuera y siguientes pasos

Ninguna arquitectura está terminada; esta tiene cinco frentes abiertos, cada uno con el módulo del curso que da las herramientas para abordarlo:

  1. Multirregión activa-activa. Hoy hay pilot light (07-03, 08-03): la región secundaria tarda 1-2 horas en servir. Pasar a activa-activa exigiría decidir el modelo de escritura por dato: km0_pedidos en Cassandra multi-DC (LOCAL_QUORUM por región, 04-04) es natural; el stock (inventario) no lo es, y obligaría a particionar la autoridad por mercado (Girona y Lleida escriben en eu-west-1, Valencia en eu-central-1) con el modelo multilíder de 03-04 para los mercados repartidos. Es un proyecto de meses y el ADR-007 pendiente.
  2. Event sourcing completo. Hoy los eventos son notificaciones de cambios de estado que se guardan aparte (y para siempre en el lago). Event sourcing haría de los eventos la fuente de verdad de pedidos: el estado se reconstruye reproduciendo pedido.creado, pago.confirmado, ... Se gana auditoría perfecta y reconstrucción de cualquier vista; se paga con snapshots, versionado de eventos históricos y consultas siempre por proyección. CQRS (08-01) ya deja el terreno preparado.
  3. Data mesh. analitica es hoy un equipo central que consume todo. A medida que crecen los dominios, cada equipo pasaría a publicar sus datos como producto (con esquema, calidad, SLA y propietario) y analitica a ser plataforma. Es la ley de Conway (08-01) aplicada a los datos.
  4. Plataforma interna como producto. El equipo de plataforma de 08-01 existe; lo que falta es tratarlo como producto: un portal donde un equipo crea un servicio nuevo con su Deployment, sus dashboards, sus alertas, su pipeline y su ADR de plantilla en minutos (el ejercicio 1 lo pone a prueba).
  5. Coste como restricción de diseño. FinOps (08-03) mide; el siguiente paso es que el coste entre en las decisiones de arquitectura como entra la latencia: euros por pedido como SLO, revisión trimestral de los servicios gestionados frente a los propios, y apagado automático de todo lo que no sirva tráfico.

  1. El proyecto: Kilómetro Cero reducido en docker-compose

El curso ha mostrado código en cada lección; el proyecto propone construir una versión reducida y funcional en el portátil, con lo que cabe en docker-compose y sin nube: catalogo, pedidos, inventario, Kafka, PostgreSQL, Redis y Prometheus. pagos se simula, reparto y analitica son opcionales, y el resto de la plataforma (Keycloak, Kubernetes, Terraform, Lambda, CDN) queda fuera del alcance mínimo. Lo importante no es el tamaño sino que cada hito sea verificable con un comando.

Estructura final del repositorio km0/

km0/
├── README.md
├── docker-compose.yml              # PostgreSQL, Kafka (KRaft), Redis, Prometheus, Grafana, los servicios
├── docs/
│   └── adr/                        # ADR-001 … ADR-006
├── contratos/
│   ├── inventario.proto            # ReservarStock, ConsultarStock, ObservarCambios (02-03)
│   └── eventos/                    # esquemas JSON de pedido.creado, stock.actualizado, ... (02-05)
├── servicios/
│   ├── comun/                      # metricas.py, logs.py, trazas.py, resiliencia.py, jwt.py
│   ├── catalogo/                   # api.py, cache.py (04-05), proyeccion_stock.py (08-01)
│   ├── pedidos/                    # api_http.py (v1/v2), saga_pedido.py (03-05), outbox_relay.py, repositorio.py
│   ├── inventario/                 # api.py (08-01), servidor_grpc.py, _dominio.py, _repositorio.py, _eventos.py
│   ├── pagos/                      # pasarela_simulada.py, consumidor.py
│   ├── reparto/                    # furgoneta_mqtt.py, puente_mqtt_kafka.py, ws_servidor.py, ws_consumidor_kafka.py
│   └── analitica/                  # consumidor_lago.py
├── sql/
│   ├── inventario/                 # referencias, reservas, outbox, mensajes_procesados
│   ├── catalogo/                   # productos, productos_vista.sql
│   └── pedidos/                    # sagas (en la versión reducida, PostgreSQL en lugar de Cassandra)
├── simulaciones/                   # relojes, particiones, quórums (Módulos 1 y 3)
├── dags/                           # km0_ventas_diarias.py (05-05)
├── serverless/                     # miniaturas/, facturas/, template.yaml, saga/ (08-04)
├── borde/
│   ├── mosquitto/                  # mosquitto.conf, acl, passwd (08-02)
│   ├── web/seguimiento.js          # cliente WebSocket (08-02)
│   ├── worker/catalogo.js          # Worker de CDN (08-04)
│   └── furgoneta/agregador.py      # (08-04)
├── certs/                          # km0-ca y certificados de desarrollo (06-04)
├── observabilidad/
│   ├── prometheus.yml, alertas.yml # (07-01)
│   └── grafana/dashboards/         # RED por servicio, saga, lag de consumidores
├── k8s/                            # pedidos-deployment.yaml, HPA, PDB, NetworkPolicy, overlays/ (07-05)
├── ansible/                        # inventario y playbooks de Kafka/Cassandra (07-05)
├── infra/aws/                      # main.tf, variables.tf, outputs.tf, entornos/ (08-03)
└── tests/
    ├── integracion/                # Testcontainers: saga, idempotencia, proyección (07-06)
    ├── contratos/                  # Pact, compatibilidad protobuf y eventos
    ├── carga/vendimia.js           # k6
    └── caos/                       # Toxiproxy, manifiestos Chaos Mesh

docker-compose.yml de la versión reducida

# km0/docker-compose.yml — versión reducida para el proyecto final
services:
  postgres:
    image: postgres:16
    environment: { POSTGRES_USER: km0, POSTGRES_PASSWORD: km0, POSTGRES_DB: km0 }
    volumes:
      - ./sql:/docker-entrypoint-initdb.d:ro      # crea km0_inventario, km0_catalogo, km0_pedidos y sus tablas
    ports: ["5432:5432"]
    healthcheck: { test: ["CMD-SHELL", "pg_isready -U km0"], interval: 5s, retries: 10 }

  kafka:
    image: apache/kafka:3.7.0                      # KRaft: sin ZooKeeper (03-03)
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
      KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_AUTO_CREATE_TOPICS_ENABLE: "false"    # los tópicos se crean explícitamente (02-04)
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
    ports: ["9092:9092"]

  kafka-init:                                      # crea los tópicos y termina
    image: apache/kafka:3.7.0
    depends_on: [kafka]
    entrypoint: ["/bin/sh", "-c"]
    command: |
      "sleep 5 &&
       /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka:9092 --create --if-not-exists --topic pedidos.eventos --partitions 6 --replication-factor 1 &&
       /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka:9092 --create --if-not-exists --topic inventario.alertas --partitions 3 --replication-factor 1 &&
       /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka:9092 --create --if-not-exists --topic pedidos.eventos.dlq --partitions 1 --replication-factor 1"

  redis:
    image: redis:7
    ports: ["6379:6379"]

  inventario:
    build: ./servicios/inventario
    environment: { PG_DSN: "postgresql://km0:km0@postgres/km0_inventario", KAFKA_BOOTSTRAP: "kafka:9092" }
    depends_on: { postgres: { condition: service_healthy } }
    ports: ["50051:50051", "9101:9100"]           # gRPC y métricas

  pedidos:
    build: ./servicios/pedidos
    environment:
      PG_DSN: "postgresql://km0:km0@postgres/km0_pedidos"
      KAFKA_BOOTSTRAP: "kafka:9092"
      INVENTARIO_GRPC: "inventario:50051"
      PAGOS_URL: "http://pagos:8002"
    depends_on: [inventario, kafka-init]
    ports: ["8001:8000", "9102:9100"]

  pagos:
    build: ./servicios/pagos                       # pasarela simulada: rechaza el 5 % y tarda 200-1500 ms
    ports: ["8002:8002"]

  catalogo:
    build: ./servicios/catalogo
    environment:
      PG_DSN: "postgresql://km0:km0@postgres/km0_catalogo"
      KAFKA_BOOTSTRAP: "kafka:9092"
      REDIS_URL: "redis://redis:6379"
    depends_on: [redis, kafka-init]
    ports: ["8003:8000", "9103:9100"]

  prometheus:
    image: prom/prometheus:v2.53.0
    volumes: ["./observabilidad/prometheus.yml:/etc/prometheus/prometheus.yml:ro"]
    ports: ["9090:9090"]

  grafana:
    image: grafana/grafana:11.1.0
    volumes: ["./observabilidad/grafana:/etc/grafana/provisioning:ro"]
    ports: ["3000:3000"]

Hitos verificables

Hito Qué construir Cómo comprobarlo
H1. Arranque docker compose up levanta todo; los tres servicios exponen /health y /metrics curl localhost:8001/health{"estado":"ok"}; curl localhost:9090/api/v1/targets muestra los tres up
H2. Catálogo con caché GET /productos/queso-curado con cache-aside en Redis (04-05) Primera petición > 5 ms y X-Cache: MISS; segunda < 1 ms y HIT; redis-cli keys 'producto:*' muestra la clave con TTL
H3. Reserva de stock por gRPC inventario.ReservarStock con SELECT FOR UPDATE, idempotente por id_reserva, publicando en outbox (02-03, 08-01) grpcurl -plaintext -d '{"id_reserva":"r1","pedido_id":"P-1","mercado":"girona","lineas":[{"producto":"queso-curado","unidades":2}]}' localhost:50051 km0.inventario.v1.Inventario/ReservarStock dos veces → misma respuesta, stock descontado una vez; SELECT * FROM outbox con stock.reservado y stock.actualizado
H4. Outbox → Kafka Relay que lee outbox y produce en pedidos.eventos con clave = pedido, marcando publicados (02-05) kafka-console-consumer --topic pedidos.eventos --from-beginning --property print.key=true muestra los eventos con su envoltura; matar el relay a mitad y reiniciar no duplica (id_evento únicos)
H5. Crear pedido con saga POST /pedidos con Idempotency-Key; saga reservar → cobrar → confirmar; compensación si pagos rechaza (03-05) curl -X POST -H 'Idempotency-Key: k1' ... dos veces → un solo pedido; con la pasarela forzada a rechazar (PAGOS_MODO=rechazar), el pedido queda rechazado y el stock vuelve (SELECT disponibles FROM referencias); SELECT * FROM sagas muestra las transiciones
H6. Proyección CQRS catalogo consume stock.actualizado, actualiza productos_vista e invalida Redis (08-01) Tras H5, GET /productos/queso-curado muestra disponible actualizado en < 2 s; SELECT * FROM mensajes_procesados crece; reenviar un evento a mano con el mismo id_evento no cambia nada
H7. Resiliencia Timeouts, reintentos con jitter y circuit breaker en pedidospagos (07-04) Parar pagos (docker compose stop pagos): las peticiones fallan rápido (< 1 s, no 30 s) tras abrirse el circuito; km0_circuit_estado{dependencia="pagos"} = 2 en Prometheus; al arrancar pagos, se cierra solo
H8. Observabilidad RED por servicio, latencia de la saga, lag de consumidores; un dashboard en Grafana; una alerta de SLO (07-01) Dashboard con rate(km0_pedidos_total[1m]) por estado y p99 de POST /pedidos; la alerta se dispara al provocar un 5 % de errores con PAGOS_MODO=errores
H9. Carga k6 con 50 usuarios virtuales creando pedidos y leyendo catálogo durante 2 minutos (07-06) p99 de catálogo < 50 ms; ningún pedido duplicado (SELECT count(*), count(DISTINCT idempotency_key) iguales); stock final = inicial − vendido
H10. Prueba de integración Testcontainers levantando PostgreSQL y Kafka; prueba de la saga con pagos que rechaza y de la idempotencia del consumidor (07-06) pytest tests/integracion en verde en menos de 2 minutos
Opcional A. Tiempo real Mosquitto + puente_mqtt_kafka.py + ws_servidor.py + seguimiento.js (08-02) Publicar con mosquitto_pub -t km0/reparto/furgoneta-3/posicion -q 1 -m '{...}' y verlo en el navegador; publicar seq menor y comprobar que se descarta
Opcional B. Analítica Consumidor que escribe eventos en ficheros Parquet por fecha; un script de Spark local que calcula ventas por productor (05-03) spark-submit ventas_diarias.py --fecha 2026-09-15 produce la tabla con la Quesería Montblanc en cabeza

Cada hito debe terminar con un ADR corto si se toma una decisión (por ejemplo, "PostgreSQL en lugar de Cassandra para pedidos en la versión reducida: contexto, consecuencias") y con las pruebas que lo verifican en tests/. El resultado es un repositorio que se puede enseñar y del que se puede hablar en una entrevista con la misma precisión con la que este curso ha hablado de Kilómetro Cero.

  1. Guía de estudio

El curso ha sido un mapa; estos son los territorios para seguir. Se citan obras y artículos por título y autor, sin enlaces: todos son fáciles de localizar por su nombre.

Libros de referencia

  • Martin Kleppmann, Designing Data-Intensive Applications (O'Reilly, 2017). El libro que más se parece a los Módulos 2, 3 y 4 de este curso, con mucha más profundidad: replicación, particionado, transacciones, consenso, procesamiento por lotes y en flujo. Es la lectura siguiente natural.
  • Andrew S. Tanenbaum y Maarten van Steen, Distributed Systems (3.ª ed., disponible gratuitamente por los autores). El texto académico clásico: modelos, comunicación, sincronización, consistencia y tolerancia a fallos con rigor formal. Complementa el Módulo 1 y el 3.
  • Sam Newman, Building Microservices (2.ª ed., O'Reilly, 2021) y Monolith to Microservices (2019). Todo lo de 08-01 con detalle: fronteras, migración, pruebas, organización.
  • Betsy Beyer et al. (eds.), Site Reliability Engineering: How Google Runs Production Systems y The Site Reliability Workbook. El origen de los SLO, los presupuestos de error, los postmortems y la guardia del Módulo 7. Disponibles gratuitamente en línea por Google.
  • Eric Evans, Domain-Driven Design (2003), y Vaughn Vernon, Implementing Domain-Driven Design (2013), para profundizar en los bounded contexts de 08-01.
  • Gregor Hohpe y Bobby Woolf, Enterprise Integration Patterns (2003): el catálogo de patrones de mensajería de los que 02-04 y 02-05 tomaron los nombres.
  • Neal Ford, Mark Richards et al., Software Architecture: The Hard Parts (2021): trade-offs de descomposición, datos distribuidos y sagas, con el mismo espíritu de ADR.

Artículos fundacionales (todos localizables por título)

  • Leslie Lamport, "Time, Clocks, and the Ordering of Events in a Distributed System" (1978): la base de 01-05. Y "The Part-Time Parliament" (1998) y "Paxos Made Simple" (2001) para el consenso.
  • Diego Ongaro y John Ousterhout, "In Search of an Understandable Consensus Algorithm" (Raft, 2014): lo que etcd y Kafka KRaft implementan (03-03).
  • Giuseppe DeCandia et al., "Dynamo: Amazon's Highly Available Key-value Store" (2007): hashing consistente, quórums, vector clocks y la elección de disponibilidad que Cassandra hereda (04-01, 04-04).
  • Jeffrey Dean y Sanjay Ghemawat, "MapReduce: Simplified Data Processing on Large Clusters" (2004), y Sanjay Ghemawat et al., "The Google File System" (2003): 05-02 y 04-02.
  • James C. Corbett et al., "Spanner: Google's Globally-Distributed Database" (2012): consistencia fuerte a escala global con relojes acotados (TrueTime), el contrapunto a Dynamo.
  • Jay Kreps, Neha Narkhede y Jun Rao, "Kafka: a Distributed Messaging System for Log Processing" (2011), y el ensayo de Jay Kreps "The Log: What every software engineer should know about real-time data's unifying abstraction" (2013): la idea del log como columna vertebral (ADR-004).
  • Hector Garcia-Molina y Kenneth Salem, "Sagas" (1987): el artículo original de 03-05.
  • Seth Gilbert y Nancy Lynch, "Brewer's Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services" (2002), y Daniel Abadi, "Consistency Tradeoffs in Modern Distributed Database System Design" (PACELC, 2012): 03-02.
  • Marc Shapiro et al., "Conflict-free Replicated Data Types" (2011): los CRDT de 03-01 y 08-04.
  • Peter Deutsch y James Gosling, las "Fallacies of Distributed Computing", y Peter Bailis et al., "Highly Available Transactions: Virtues and Limitations" (2013), para seguir con las falacias y los modelos de aislamiento.
  • Tyler Akidau et al., "The Dataflow Model" (2015): tiempo de evento, marcas de agua y ventanas de 05-04.

Práctica

  • Jepsen (Kyle Kingsbury): análisis públicos de cómo fallan bases de datos reales bajo particiones; la mejor escuela de escepticismo sobre garantías anunciadas.
  • Los libros de Michael Nygard, Release It! (2.ª ed., 2018), para los patrones de estabilidad de 07-04 contados desde incidentes reales.
  • Construir el proyecto del apartado 9 y, después, romperlo con los experimentos de 07-06.

Errores Comunes y Consejos

  • Presentar la arquitectura como un diagrama sin decisiones. El diagrama del apartado 1 vale poco sin los ADR del apartado 5; lo que se aprende de una arquitectura son sus alternativas rechazadas y sus consecuencias aceptadas.
  • Evaluar por tecnologías y no por criterios. "Usa Kafka y Kubernetes" no dice si está bien diseñada. Las tablas del apartado 7 (falacias, consistencia por dato, SLO, RPO/RTO, quién puede qué) sí.
  • Empezar el proyecto por la infraestructura. Kubernetes, mesh y Terraform sin un servicio que desplegar es el error de "elegir tecnologías antes que problemas" de 01-06. El proyecto empieza en H1 con tres servicios y docker-compose.
  • Creer que la plataforma está terminada. El apartado 8 lista cinco frentes; una arquitectura sin lista de pendientes es una que nadie está mirando.
  • Consejo: en cualquier sistema nuevo, empieza por escribir los síntomas (01-06), la tabla de consistencia por dato (03-01) y los SLO (07-01) antes de dibujar cajas. Son las tres páginas que más decisiones correctas producen.
  • Consejo: guarda este curso como referencia por lección: cuando dentro de un año tengas que decidir entre orquestación y coreografía, o cuánto TTL poner en una CDN, la lección correspondiente tiene la tabla.

Ejercicios

Ejercicio 1: incorporar el dominio "valoraciones"

En 08-01 se decidió que las valoraciones son un bounded context propio con comunicación por eventos. Ahora diséñalo de extremo a extremo aplicando todo el curso: (a) contrato de eventos que consume y produce (con la envoltura de 02-04) y modelo de datos con su modelo de consistencia (03-01) y su almacenamiento (Módulo 4); (b) API pública en Kong con autenticación y autorización (quién puede valorar qué; 06-01, 06-05) y cómo llega la media a la ficha cacheada por la CDN (08-04); (c) SLO, métricas y alertas (07-01), y qué trazas y auditoría deja una valoración; (d) despliegue (07-05), RPO/RTO (07-03) y qué habría que añadir a main.tf (08-03); (e) el ADR-007 en formato resumido (decisión, alternativa rechazada, consecuencia).

Ejercicio 2: incidente de extremo a extremo

Sábado 19 de septiembre, 18:42, Semana del Queso Artesano. Se dispara la alerta "SLO de pedidos: consumo de presupuesto de error ×14 en 5 min". Datos disponibles:

  • Prometheus: rate(km0_pedidos_total{estado="rechazado_stock"}[5m]) pasa de 0,3/s a 9/s a las 18:40; la latencia p99 de POST /pedidos baja de 420 ms a 95 ms; km0_grpc_client_duration_seconds{metodo="ReservarStock"} p99 = 8 ms; km0_circuit_estado{dependencia="inventario"} = 0; lag del grupo catalogo-proyeccion = 0.
  • Logs de inventario (Loki): a partir de las 18:39:50, cientos de líneas {"nivel":"warning","msg":"StockInsuficiente","producto":"queso-curado","mercado":"girona","disponibles":0,"solicitadas":2,"request_id":...}.
  • Logs de catalogo: {"nivel":"info","msg":"vista actualizada","producto":"queso-curado","mercado":"girona","aplicado":true} a las 18:39:48, y ninguno después para ese producto.
  • Trazas: los pedidos rechazados tienen spans kong → pedidos → inventario.ReservarStock (StockInsuficiente) de 90 ms; ninguno llega a pagos.
  • productos_vista en catalogo: queso-curado / girona: disponible = true, unidades_aprox = 0, version_stock = 1758300000123.
  • Auditoría: a las 18:39:45, {"quien":"mpuig","accion":"stock.ajustar","recurso":"queso-curado/girona","de":140,"a":0,"origen":"panel-productor"}.

(a) Reconstruye la cadena causal completa. (b) ¿Es un incidente de la plataforma, del producto o de operación? ¿Qué parte del comportamiento es correcta y cuál es un defecto? (c) Localiza el defecto exacto en el código de 08-01 (proyeccion_stock.py o productos_vista.sql) y corrígelo. (d) Escribe las tres acciones del postmortem (07-03) y qué señal habría detectado el problema antes que el SLO.

Ejercicio 3: recortar la arquitectura para tres personas

Una startup de tres personas quiere lanzar un marketplace como Kilómetro Cero en un solo mercado, con 20 productores, 300 pedidos al día y 6 repartidores. Tienen tres meses y no pueden operar Kubernetes ni Kafka. Usando la tabla de "cuándo no usar microservicios" de 08-01 y el lock-in selectivo de 08-03, diseña la arquitectura mínima: (a) qué se queda como monolito modular y con qué estructura interna; (b) qué tres decisiones de Kilómetro Cero conservarías intactas porque son baratas y evitan errores irreversibles; (c) cómo resolverías el seguimiento en tiempo real y las fotos sin Kafka, MQTT ni Lambda; (d) qué señales medibles indicarían que ha llegado el momento de extraer el primer servicio, y cuál sería.

Soluciones

Ejercicio 1.

(a) Consume pedido.entregado (publicado por reparto en pedidos.eventos con la envoltura estándar; datos: {pedido_id, cliente, lineas: [{producto, productor}], entregado_ms}) para saber qué se puede valorar. Produce en un tópico nuevo valoraciones.eventos (3 particiones, clave = producto, para que las valoraciones de un producto lleguen en orden a la proyección): valoracion.creada {valoracion_id, cliente, pedido_id, producto, productor, puntos, comentario, fecha_ms} y valoracion.respondida {valoracion_id, productor, respuesta, fecha_ms}. Modelo: tabla valorables (cliente, pedido_id, producto, entregado_ms, valorado bool) y valoraciones (id, cliente, producto, productor, puntos, comentario, respuesta, creada_en) en una base PostgreSQL propia km0_valoraciones (RDS pequeña, sin Multi-AZ al inicio): volumen bajo (una por línea entregada), consultas relacionales (por producto, por productor, pendientes de respuesta), y la regla "una valoración por línea" exige una restricción UNIQUE (cliente, pedido_id, producto) con consistencia fuerte local. La media en la ficha es eventual (proyección en catalogo), y el panel del productor lee su propia base con consistencia fuerte.

(b) Rutas en Kong: POST /api/v1/valoraciones (rol cliente), GET /api/v1/productos/{slug}/valoraciones (pública, cacheable 60 s), POST /api/v1/valoraciones/{id}/respuesta (rol productor), GET /api/v1/productores/me/valoraciones?pendientes=true (rol productor). Autorización en el servicio: al crear, claims.sub debe tener una fila en valorables no valorada para ese pedido y producto (ABAC con dato propio, sin llamar a pedidos); al responder, claims.productor debe coincidir con valoraciones.productor. Rate limiting específico (5 valoraciones/minuto por consumidor) contra el abuso. La media llega a la ficha porque catalogo consume valoracion.creada y mantiene suma_puntos y num_valoraciones en productos_vista (idempotente por id_evento); la ficha sigue siendo public, max-age=60 por mercado, y la media cambia con hasta un minuto de retraso en la CDN, aceptable. Los tres últimos comentarios se sirven en la ruta pública de valoraciones (cacheable, sin datos personales más allá del nombre de pila, que el cliente consintió).

(c) SLO: 99,5 % de disponibilidad y p99 < 300 ms para crear (no está en el camino de compra: SLO más laxo que pedidos). Métricas: km0_valoraciones_total{resultado}, latencia por ruta, lag del consumidor de pedido.entregado y del de catalogo sobre valoraciones.eventos. Alertas: consumo de presupuesto de error, lag > 5 min (una entrega que no se puede valorar), y tasa de no_autorizado anómala (abuso). Traza: kong → valoraciones POST → postgres, enlazada al consumidor de catalogo por el id_evento. Auditoría: valoracion.crear y valoracion.responder en auditoria.eventos (las respuestas de productores son contenido público, y la moderación necesita saber quién escribió qué).

(d) k8s/valoraciones-deployment.yaml copiando la plantilla de pedidos (2 réplicas, sondas, resources, topologySpreadConstraints, HPA por CPU, NetworkPolicy que solo permite entrada desde Kong y salida a su RDS y a Kafka, serviceAccountName con IRSA sin permisos de S3). RPO: minutos (backups de RDS y PITR; la pérdida de valoraciones es tolerable pero molesta); RTO: 1 hora (no bloquea ventas). En main.tf: aws_db_instance.valoraciones (db.t4g.small, sin multi_az), su grupo de seguridad (5432 solo desde EKS), un módulo IRSA sin políticas de S3, y el tópico valoraciones.eventos gestionado con el proveedor kafka o creado por el pipeline.

(e) ADR-007: "valoraciones como bounded context propio con PostgreSQL y comunicación exclusivamente por eventos". Alternativa rechazada: dentro de catalogo (acoplaría la moderación a los despliegues del catálogo en campaña y mezclaría dos lenguajes: "producto publicado" y "valoración"). Consecuencia aceptada: la ficha muestra la media con hasta un minuto de retraso; un componente más con base de datos, dashboard y guardia (del equipo de catálogo y productores); si reparto deja de publicar pedido.entregado, nadie puede valorar hasta que se recupere el lag (alerta).

Ejercicio 2.

(a) Cadena causal: 18:39:45, Marta Puig ajusta desde su panel el stock de queso-curado en Girona de 140 a 0 (probablemente un error al introducir el dato, o una retirada real del producto). inventario aplica el ajuste y publica stock.actualizado {disponible: 0}. 18:39:48, catalogo recibe el evento y la proyección lo aplica (aplicado: true) → unidades_aprox = 0. Pero disponible sigue en true. A partir de 18:39:50, los clientes que ven la ficha (con disponible = true) añaden queso-curado al carrito y confirman; pedidos inicia la saga, inventario responde StockInsuficiente en 8 ms, la saga compensa y el pedido queda rechazado_stock. La latencia p99 baja (los pedidos rechazados no pasan por la pasarela), la tasa de rechazos sube ×30 y el presupuesto de error se consume porque rechazado_stock cuenta como fallo del SLO de pedidos.

(b) Es un incidente mixto. La saga, el circuito y la compensación funcionan correctamente: ningún pedido se cobró sin stock, ninguna reserva quedó colgada. El ajuste de Marta es una acción legítima de negocio (y la auditoría la registra con precisión). El defecto está en la plataforma: la ficha del catálogo sigue diciendo "disponible" con 0 unidades, lo que dirige a cientos de clientes a un rechazo seguro. Es un defecto de la proyección CQRS, no de la consistencia eventual: el retraso fue de 3 segundos, aceptable; el problema es que el dato proyectado es incorrecto de forma permanente.

(c) En proyeccion_stock.py, aplicar calcula disp = d["disponible"] >= UMBRAL_DISPONIBLE con UMBRAL_DISPONIBLE = 1: con disponible = 0, disp = False, y el UPDATE debería haber puesto disponible = FALSE... salvo que la condición version_stock < %(v)s haya fallado. Pero el log dice aplicado: true, así que el UPDATE afectó a la fila. Entonces el defecto no está ahí; mírese productos_vista.sql: disponible BOOLEAN NOT NULL DEFAULT FALSE y unidades_aprox se actualizan juntos en el mismo UPDATE. La fila muestra unidades_aprox = 0 y disponible = true, lo que solo es posible si otra ruta de escritura puso disponible = true después: la ruta de publicación del producto por el productor (catalogo crea/actualiza la fila cuando el productor edita la ficha y, según 08-01, "la fila la crea catalogo cuando el productor publica el producto"). Marta editó la ficha (quizá el mismo ajuste desde el panel toca producto y stock), y el UPSERT de publicación sobrescribe disponible con su valor por defecto o con el valor que tenía la ficha en el formulario, sin respetar version_stock. El defecto es que dos caminos escriben la misma columna con reglas distintas: la corrección es que la publicación de la ficha nunca escriba disponible ni unidades_aprox (columnas propiedad exclusiva de la proyección), es decir, INSERT ... ON CONFLICT (slug, mercado) DO UPDATE SET nombre = EXCLUDED.nombre, precio_cents = EXCLUDED.precio_cents, productor = EXCLUDED.productor sin tocar las columnas de stock; y, como red de seguridad, un CHECK (NOT disponible OR unidades_aprox >= 1) en la tabla que haría fallar la escritura incoherente en lugar de dejarla pasar. Es el principio de 08-01 de "dueño del dato" aplicado dentro de un servicio, columna a columna.

(d) Postmortem sin culpa (07-03): 1. Corregir el UPSERT de publicación y añadir el CHECK; prueba de integración (07-06) que publica la ficha después de un stock.actualizado a 0 y verifica disponible = false. 2. Añadir una confirmación en el panel del productor para ajustes de stock que cambien más de un 50 % ("¿Poner queso-curado en Girona a 0 unidades? Había 140"), porque la auditoría muestra un salto improbable. 3. Separar en el SLO de pedidos los rechazos por stock (resultado de negocio correcto) de los errores de plataforma, o crear una alerta específica de "tasa de rechazado_stock por producto" con umbral bajo, para que el síntoma se atribuya bien. La señal que lo habría detectado antes: una alerta de coherencia en la proyección, count(productos_vista where disponible and unidades_aprox = 0) > 0, evaluada cada minuto (una consulta barata), o la métrica de Flink de 05-04 alertas_stock para queso-curado en Girona, que se disparó a las 18:39:50 hacia inventario.alertas y que Marta recibió por SSE (08-02) pero que nadie correlacionó con los rechazos.

Ejercicio 3.

(a) Un monolito modular en Python (FastAPI) con una base de datos PostgreSQL gestionada (RDS o Cloud SQL con backups automáticos y PITR, sin Multi-AZ al principio), desplegado en un PaaS de contenedores (Cloud Run, App Runner, Fly.io, Render: 2 instancias, autoescalado incluido, TLS gestionado). Estructura interna: los seis paquetes de 01-06 (catalogo, pedidos, inventario, pagos, reparto, analitica) cada uno con su api.py como único módulo público (08-01), esquemas de PostgreSQL separados por paquete (inventario.referencias, pedidos.pedidos) y una regla de lint (import-linter) que prohíbe importar módulos privados de otro paquete y consultas SQL entre esquemas. La transacción de confirmar pedido es local (una sola base): reserva, pedido y registro del cobro en una transacción, con la llamada a la pasarela fuera de ella (cobrar primero con Idempotency-Key, luego confirmar en transacción; si falla la confirmación, reembolsar). Redis gestionado pequeño para la caché del catálogo y el limitador. Un solo repositorio, un pipeline, docker-compose para desarrollo.

(b) Tres decisiones baratas que evitan errores irreversibles: 1. La envoltura de eventos y una tabla eventos (outbox) desde el primer día, aunque el "bus" sea la propia base de datos y el consumidor un proceso del monolito: cuando llegue Kafka, los productores ya publican y los consumidores ya son idempotentes; y el lago analítico puede empezar exportando esa tabla a Parquet cada noche. 2. Idempotencia en las APIs de escritura (Idempotency-Key en crear pedido, claves deterministas hacia la pasarela): es lo que en 02-05 y 08-04 evita cobros y pedidos duplicados, cuesta una tabla, y no se puede añadir después sin migrar clientes. 3. Observabilidad mínima con SLO: logs JSON con request_id, métricas RED y un SLO escrito para crear pedido; sin esto, no habrá datos para decidir cuándo extraer nada (apartado d). Y una cuarta gratuita: los ADR desde el primer día.

(c) Seguimiento: 6 repartidores enviando posiciones por HTTPS POST cada 10 s al monolito (60 peticiones/minuto: nada), guardadas en una tabla reparto.posiciones con retención de 24 h (a este volumen, PostgreSQL sobra), y los clientes con SSE o polling cada 5 s contra un endpoint que devuelve la última posición del pedido (con Cache-Control: max-age=4 para que el PaaS o una CDN absorba repeticiones). Sin MQTT ni WebSockets: no hay volumen que lo justifique, y SSE con reconexión automática cubre la experiencia. Fotos: subida por URL prefirmada a S3/GCS (04-03, se conserva) y miniaturas generadas en el mismo monolito en una tarea en segundo plano (una cola simple en PostgreSQL con SELECT ... FOR UPDATE SKIP LOCKED, procesada por un worker del propio despliegue), con una CDN gratuita o de bajo coste delante del bucket con claves inmutables (08-04, se conserva porque es casi gratis y evita el egress).

(d) Señales para extraer el primer servicio, medibles gracias a (b)-3: el p99 de crear pedido se degrada cuando sube el tráfico del catálogo (contención en la misma base o CPU: síntoma 1); el equipo pasa de 3 a 8-10 personas y los despliegues del catálogo rompen pedidos (síntoma 3); el tráfico de lectura supera lo que Redis + una réplica de lectura de PostgreSQL absorben; o la latencia de la pasarela empieza a bloquear conexiones (síntoma 2). El primero en salir, como en 01-06, es catalogo (solo lecturas, sin transacciones cruzadas, vuelta atrás con un cambio de ruta) o, si el síntoma dominante es el de la pasarela, pagos con su ACL. Nunca pedidos e inventario primero: son los que exigen sagas, y las sagas se justifican solo cuando la frontera ya existe por otra razón. La regla de 08-01 se cumple al revés: tres personas sin síntomas es la fila exacta de la tabla que dice "monolito modular".

Conclusión

Kilómetro Cero empezó como un monolito Python con una base de datos y cinco síntomas, y termina como una plataforma que puede dibujarse en un diagrama, recorrerse con un pedido, justificarse con seis ADR y evaluarse con los criterios que el propio curso ha dado: las ocho falacias asumidas como falsas en puntos concretos del diseño, una tabla de consistencia por dato donde "fuerte" aparece dos veces y "eventual" seis, SLO y RPO/RTO por componente, y una tabla de quién puede qué desde Ana hasta el Worker de la CDN. Los cinco síntomas de 01-06 tienen cada uno su resolución y su forma de verificarla; las decisiones tienen cada una su alternativa rechazada y su precio; y lo que queda fuera está escrito, con el módulo que da las herramientas para abordarlo. El proyecto propuesto reduce todo eso a lo que cabe en un portátil, con diez hitos que se comprueban con un comando, y la guía de estudio señala los libros y artículos donde cada idea de este curso tiene su origen y su profundidad.

Lo que el alumno sabe hacer ahora no es "usar Kafka" o "desplegar en Kubernetes", aunque también. Es algo más duradero: ante un sistema que tiene que repartirse entre varias máquinas, sabe escribir primero los síntomas y no las tecnologías; cortar fronteras donde el lenguaje cambia de significado y no donde hay una tabla; elegir para cada dato el modelo de consistencia que necesita y pagar solo por ese; hacer que cada llamada tenga timeout, cada mensaje un identificador y cada consumidor sea idempotente, porque la red va a fallar; sustituir la transacción global por una saga que compensa; poner el dato donde su carga lo pide, de PostgreSQL a Cassandra, de Redis a S3, de la CDN a la cola SQLite de una furgoneta; separar lo que se calcula por lotes de lo que no puede esperar; autenticar y autorizar en cada frontera sin confiar en ninguna; medir con SLO, observar con trazas, recuperar con backups probados y ensayar los fallos antes de que ocurran; describir la infraestructura como código y el coste como una métrica; y dejar cada decisión escrita con su contexto y sus consecuencias, para que quien venga después pueda cambiarla con conocimiento.

Eso es diseñar arquitecturas distribuidas: no acumular piezas, sino saber en cada una qué se gana, qué se pierde y por qué se eligió. Kilómetro Cero ha sido la excusa; el criterio es lo que se lleva el alumno. Gracias por haber recorrido el curso hasta aquí, y buena suerte con el siguiente sistema que tenga que repartirse entre varias máquinas: ahora ya sabe por dónde empezar.

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