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
- La arquitectura final de Kilómetro Cero
- La tabla de 01-06, cerrada
- El recorrido de un pedido de Ana de extremo a extremo
- Lo que el pedido deja a su paso: trazas, métricas y auditoría
- Las decisiones de arquitectura como ADR
- Los cinco síntomas de 01-06, revisitados
- Evaluación con los criterios del curso
- Lo que queda fuera y siguientes pasos
- El proyecto: Kilómetro Cero reducido en
docker-compose - Guía de estudio
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- 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.
- 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.posiciones → reparto.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 |
- 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:
- 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.
- 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).
- La idempotencia aparece cuatro veces: la
Idempotency-Keyde Ana en Kong/pedidos(un doble clic no crea dos pedidos), elid_reservaeninventario, la clavecobro-<pedido>hacia la pasarela, y elid_eventoen cada consumidor. Un reintento en cualquier punto es seguro. - 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).
- 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.
- 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: kong → pedidos POST /pedidos → inventario.ReservarStock (12 ms) → pagos.Cobrar (1 210 ms, con span hijo pasarela) → cassandra.insert → outbox.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.eventos → km0-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.
- 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.
- 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 pedidos → pagos (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 |
- 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) |
- 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:
- 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_pedidosen Cassandra multi-DC (LOCAL_QUORUMpor 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 eneu-west-1, Valencia eneu-central-1) con el modelo multilíder de 03-04 para los mercados repartidos. Es un proyecto de meses y el ADR-007 pendiente. - 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 reproduciendopedido.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. - Data mesh.
analiticaes 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) yanaliticaa ser plataforma. Es la ley de Conway (08-01) aplicada a los datos. - 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).
- 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.
- El proyecto: Kilómetro Cero reducido en
docker-compose
docker-composeEl 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 Meshdocker-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 pedidos → pagos (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.
- 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 dePOST /pedidosbaja de 420 ms a 95 ms;km0_grpc_client_duration_seconds{metodo="ReservarStock"}p99 = 8 ms;km0_circuit_estado{dependencia="inventario"}= 0; lag del grupocatalogo-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 apagos. productos_vistaencatalogo: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
- Conceptos Básicos de Sistemas Distribuidos
- Modelos de Sistemas Distribuidos
- Ventajas y Desafíos de los Sistemas Distribuidos
- Las Falacias de la Computación Distribuida
- Tiempo, Relojes y Ordenación de Eventos
- Del Monolito a la Plataforma Distribuida: el Caso Kilómetro Cero
Módulo 2: Comunicación en Sistemas Distribuidos
- Protocolos de Comunicación
- RPC y RMI
- gRPC y Serialización de Datos
- Mensajería y Colas de Mensajes
- Patrones de Comunicación Asíncrona
Módulo 3: Consistencia y Replicación
- Modelos de Consistencia
- El Teorema CAP y PACELC
- Algoritmos de Consenso
- Replicación de Datos
- Transacciones Distribuidas y Sagas
Módulo 4: Almacenamiento Distribuido
- Particionado de Datos y Hashing Consistente
- Sistemas de Archivos Distribuidos
- Almacenamiento de Objetos
- Bases de Datos Distribuidas
- Cachés Distribuidos
Módulo 5: Computación Distribuida
- Modelos de Computación Distribuida
- MapReduce y Hadoop
- Spark y Computación en Memoria
- Procesamiento de Flujos de Datos
- Planificación de Trabajos y Pipelines de Datos
Módulo 6: Seguridad en Sistemas Distribuidos
- Autenticación y Autorización
- Cifrado y Protección de Datos
- Gestión de Identidades
- Seguridad entre Servicios: mTLS y Gestión de Secretos
- Puertas de Enlace, Limitación de Tasa y Auditoría
Módulo 7: Monitoreo y Mantenimiento
- Monitoreo de Sistemas Distribuidos
- Logs Centralizados y Trazabilidad Distribuida
- Gestión de Fallos y Recuperación
- Patrones de Resiliencia: Timeouts, Reintentos y Circuit Breaker
- Automatización y Orquestación
- Pruebas en Sistemas Distribuidos e Ingeniería del Caos
