En la lección anterior partimos PideYa en servicios, pero dejamos pendiente la pregunta de fondo: ¿cómo hablan entre sí, y qué pasa cuando la conversación falla? Un sistema distribuido es aquel en el que el fallo de una máquina que ni conocías puede dejar tu programa inservible (la célebre definición de Leslie Lamport). Esta lección cubre los patrones de comunicación y de datos entre nodos: del proxy remoto que prometimos en el módulo 3 a la mensajería asíncrona, las garantías de entrega, el patrón Outbox y la consistencia eventual. El hilo GoF continúa: aquí Proxy y Observer se estiran sobre la red hasta casi no reconocerse — pero la intención es la misma.
Contenido
- Proxy remoto y RPC: la llamada que cruza la red
- Las 8 falacias de la computación distribuida
- Mensajería asíncrona: colas productor-consumidor
- Pub-sub y brokers: Observer a escala de red
- Garantías de entrega e idempotencia
- El patrón Outbox
- Arquitectura orientada a eventos en PideYa
- Consistencia eventual y el teorema CAP
- Resiliencia complementaria: timeout, bulkhead y dead letter queue
Proxy remoto y RPC
En 03-08 dejamos una puerta abierta: el proxy remoto, prometido para esta lección. La idea de RPC (Remote Procedure Call) es que llamar a otro proceso parezca llamar a un método local:
// El cliente del servicio de Pagos usa la MISMA interfaz que el dominio
PasarelaPago pagos = clienteRpc.crearProxy(PasarelaPago.class, "http://pagos:8080");
ResultadoCobro r = pagos.cobrar(pedido, tarjeta); // parece local... pero viaja por redEse crearProxy genera un Proxy GoF (con los proxies dinámicos que vimos en 03-08) que serializa los argumentos, los envía por HTTP o gRPC, espera la respuesta y la deserializa. Los stubs de gRPC, los clientes Feign de Spring o los viejos RMI son exactamente esto: el patrón Proxy con la red dentro.
La trampa es que la transparencia es mentira. Una llamada local falla de una forma (excepción); una remota falla de tres: antes de llegar, al ejecutarse, o al volver la respuesta — y en el tercer caso el efecto sí ocurrió aunque tú veas un timeout. Este "fallo parcial" no existe dentro de un proceso y es la razón de casi todos los patrones de esta lección.
Las 8 falacias de la computación distribuida
Formuladas en Sun Microsystems (Deutsch, Gosling y otros), son las suposiciones falsas que todo el mundo hace al principio:
| # | Falacia | Consecuencia real en PideYa |
|---|---|---|
| 1 | La red es fiable | Los mensajes se pierden: hacen falta reintentos y confirmaciones |
| 2 | La latencia es cero | 200 llamadas al catálogo para pintar la carta = pantalla lentísima |
| 3 | El ancho de banda es infinito | Enviar el pedido entero en cada evento satura el broker |
| 4 | La red es segura | Los servicios deben autenticarse entre sí (mTLS, tokens) |
| 5 | La topología no cambia | Las IPs cambian con cada despliegue: de ahí el Service Discovery de 06-02 |
| 6 | Hay un solo administrador | El proveedor cloud reinicia nodos sin avisarte |
| 7 | El coste de transporte es cero | Serializar/deserializar consume CPU real |
| 8 | La red es homogénea | El datacenter y el móvil del repartidor en un túnel no se parecen |
Grábate la 1 y la 2: justifican todo lo que viene a continuación.
Mensajería asíncrona: colas productor-consumidor
La alternativa al RPC síncrono es no esperar: el emisor deja un mensaje en una cola y sigue con su vida; un consumidor lo procesa cuando puede.
flowchart LR
PED[Servicio Pedidos] -- "mensaje: PrepararPedido" --> Q[(Cola de cocina)]
Q --> C1[Consumidor cocina 1]
Q --> C2[Consumidor cocina 2]
Es el patrón productor-consumidor: cada mensaje lo procesa un solo consumidor (los dos trabajadores de cocina compiten por los mensajes). Ventajas frente al RPC:
- Desacoplamiento temporal: si cocina está caída cinco minutos, los pedidos esperan en la cola en vez de perderse.
- Nivelación de carga: el pico de las 21:00 se encola; los consumidores lo procesan a su ritmo (y puedes añadir consumidores).
- El precio: el productor no sabe cuándo (ni si) se procesó su mensaje — la respuesta, si la hay, llega como otro mensaje.
Aviso importante: este mismo patrón existe dentro de un proceso con hilos y BlockingQueue; esa versión local es materia de la próxima lección, 06-04.
Pub-sub y brokers: Observer a escala de red
En 04-08 dejamos una puerta abierta hacia el pub-sub; aquí se cruza. En publish-subscribe, el emisor publica eventos en un topic y todos los suscriptores reciben una copia. Es exactamente la intención del Observer — notificar a interesados desconocidos sin acoplarse a ellos — con dos diferencias:
- Entre sujeto y observadores se interpone un broker (un intermediario dedicado), y
- los observadores viven en otros procesos y pueden estar caídos al publicarse el evento.
| Observer (GoF, 04-08) | Pub-sub distribuido | |
|---|---|---|
| Registro | pedido.suscribir(observador) en memoria |
Suscripción a un topic del broker |
| Entrega | Llamada síncrona, mismo hilo | Asíncrona, por red, con reintentos |
| ¿El sujeto conoce a los observadores? | Tiene la lista de referencias | No sabe ni cuántos hay |
| Si el observador falla | La excepción puede romper la notificación | El broker reintenta o aparca el mensaje |
Los dos brokers que debes conocer, a nivel conceptual:
- RabbitMQ: broker de colas clásico; enruta mensajes hacia colas y los borra al confirmarse. Bueno para trabajo distribuido y RPC asíncrono.
- Kafka: un log de eventos duradero y particionado; los mensajes no se borran al leerse, cada consumidor recuerda su posición (offset) y puede releer el pasado. Eso lo empareja de forma natural con el Event Sourcing que vimos en 06-01.
Garantías de entrega e idempotencia
¿Cuántas veces llega un mensaje? Las tres respuestas posibles:
| Garantía | Significa | Coste |
|---|---|---|
| At-most-once | 0 o 1 veces: se envía y se olvida | Puedes perder mensajes |
| At-least-once | 1 o más veces: se reintenta hasta confirmar | Puedes recibir duplicados |
| Exactly-once | Exactamente 1 vez | Muy caro o imposible en general; se simula |
La opción práctica es casi siempre at-least-once + consumidores idempotentes: aceptas duplicados y haces que procesar dos veces sea inocuo. Es la misma disciplina de idempotencia que aplicamos a los reintentos de 06-02, ahora en el lado consumidor:
public void alRecibir(EventoPedidoCobrado evento) {
// Idempotencia: si ya procesamos este id de evento, ignorar el duplicado
if (procesados.existe(evento.idEvento())) return;
cocina.encolarComanda(evento.idPedido());
procesados.registrar(evento.idEvento()); // misma transacción que el efecto
}El "exactly-once" que anuncian algunas plataformas es, en la práctica, at-least-once con deduplicación automática: la idempotencia no desaparece, solo cambia de sitio.
El patrón Outbox
Problema sutil y letal: el servicio Pedidos debe (a) guardar el pedido en su BD y (b) publicar PedidoConfirmado en el broker. Son dos sistemas distintos: si guarda y luego se cae antes de publicar, el pedido existe pero nadie se entera (la cocina jamás lo prepara). No hay transacción que abarque BD y broker.
El patrón Transactional Outbox lo resuelve con una sola transacción... en la BD:
- En la misma transacción que guarda el pedido, se inserta el evento en una tabla
outbox. - Un proceso aparte (relay) lee la tabla
outbox, publica los eventos en el broker y los marca como enviados. - Si el relay se cae, reintenta: entrega at-least-once, que ya sabemos manejar con idempotencia.
Atomicidad garantizada por la BD, entrega garantizada por el reintento. Herramientas como Debezium hacen de relay leyendo directamente el log de transacciones de la base de datos (Change Data Capture).
Arquitectura orientada a eventos en PideYa
Juntemos las piezas: el viaje del pedido, que en 05-02 era una secuencia de llamadas dentro del monolito, ahora es una cadena de eventos:
sequenceDiagram
participant P as Pedidos
participant B as Broker
participant PA as Pagos
participant C as Cocina
participant R as Reparto
participant N as Notificaciones
P->>B: publica PedidoConfirmado (vía outbox)
B->>PA: PedidoConfirmado
PA->>B: publica PedidoCobrado
B->>C: PedidoCobrado
B->>N: PedidoCobrado (email al cliente)
C->>B: publica PedidoListo
B->>R: PedidoListo
R->>B: publica PedidoEntregado
B->>N: PedidoEntregado (notifica al cliente)
B->>P: PedidoEntregado (cierra el ciclo de vida)
Observa tres cosas. Primera: nadie llama a nadie — cada servicio publica hechos y reacciona a hechos, como los observadores de 04-08 pero sin lista de suscriptores en memoria. Segunda: añadir un servicio de estadísticas (nuestro viejo ObservadorEstadisticas) es suscribirse a los topics, sin tocar ni una línea de los demás. Tercera: esto es una saga coreografiada de las de 06-02, vista desde la tubería.
Consistencia eventual y el teorema CAP
En este mundo, cuando el cliente pregunta "¿dónde está mi pedido?", la pantalla puede ir unos segundos por detrás de la realidad: el evento PedidoListo existe pero aún no ha llegado a la vista de lectura. Eso es consistencia eventual: si dejan de producirse escrituras, todas las réplicas acaban convergiendo — pero mientras tanto, se puede leer el pasado.
El teorema CAP (Brewer) explica por qué no es un defecto arreglable, a nivel introductorio: ante una Partición de red (nodos incomunicados, y la falacia 1 garantiza que ocurrirá), un sistema distribuido debe elegir entre Consistencia (rechazar operaciones para no divergir) y Availability/disponibilidad (responder con datos posiblemente desactualizados). PideYa elige por caso: el seguimiento del pedido prefiere disponibilidad (mejor un estado con 5 s de retraso que un error), el cobro prefiere consistencia (mejor rechazar que cobrar dos veces).
Resiliencia complementaria
Tres patrones que completan el circuit breaker y el retry de 06-02 (no los repetiremos aquí):
- Timeout: toda llamada remota lleva un límite de espera explícito. Sin timeout, un servicio colgado te cuelga a ti; el circuit breaker de 06-02 cuenta los timeouts como fallos. Regla: el timeout del llamador debe ser mayor que el del llamado, o cancelarás trabajo que iba a llegar.
- Bulkhead (mamparo, como los compartimentos estancos de un barco): aísla recursos por dependencia — un pool de conexiones para llamar a Pagos y otro distinto para el Catálogo. Si Pagos se atasca, agota su pool, no el del resto de la aplicación. Es la intención de aislar para que el fallo no se propague, hermana del confinamiento que veremos en concurrencia.
- Dead Letter Queue (DLQ): cuando un mensaje falla repetidamente (un evento malformado, un bug del consumidor), reintentarlo para siempre bloquea la cola. Tras N intentos se aparta a una cola de "cartas muertas" donde un humano o un proceso lo examina. Sin DLQ, un solo mensaje venenoso (poison message) puede parar la cocina entera de PideYa.
Errores Comunes y Consejos
- Creerse la transparencia del RPC: tratar una llamada remota como local, sin timeout ni plan para el fallo parcial. Toda llamada que cruza la red necesita: timeout, política de reintento (solo si es idempotente) y comportamiento definido cuando falle.
- Publicar al broker fuera de la transacción: el bug silencioso que motiva el Outbox. Si guardas en BD y publicas después "a mano", tarde o temprano divergirán. Síntoma: pedidos que existen pero que ningún otro servicio conoce.
- Suponer orden global de eventos: los brokers suelen garantizar orden solo por partición/clave. Diseña los consumidores para tolerar
PedidoListollegando antes quePedidoCobrado, o particiona por id de pedido. - Ignorar los duplicados "porque casi nunca pasan": con at-least-once, los duplicados son cuestión de tiempo. La deduplicación por id de evento debe estar desde el primer día, y en la misma transacción que el efecto.
- Eventos gordos: publicar el pedido completo con la carta embebida (falacia 3). Publica hechos con ids y lo mínimo necesario; quien necesite más, que consulte.
- DLQ como agujero negro: aparcar mensajes está bien; no monitorizar la DLQ convierte fallos visibles en pérdidas invisibles. Alarma cuando la DLQ crezca.
Ejercicios
- Diagnóstico de fallo parcial. Pedidos llama por RPC a Pagos con timeout de 3 s. La llamada lanza
TimeoutException. Enumera los tres escenarios posibles sobre lo que ocurrió en Pagos y qué debería hacer Pedidos para reintentar con seguridad. - ¿Cola o topic? Para cada necesidad de PideYa, decide entre cola productor-consumidor o topic pub-sub, y justifica: (a) repartir las comandas entre las tres instancias del servicio de cocina; (b) avisar a notificaciones, estadísticas y facturación de que un pedido se entregó; (c) procesar los reembolsos pendientes uno a uno.
- Aplica el Outbox. Escribe el pseudocódigo (o Java esquemático) del método
confirmarPedidodel servicio Pedidos usando el patrón Outbox, y del relay que publica. Indica qué garantía de entrega resulta y qué debe hacer el consumidor por ello.
Soluciones
- Escenarios: (a) la petición nunca llegó a Pagos — no hubo cobro; (b) llegó y Pagos falló a mitad — puede o no haber cobro; (c) Pagos cobró correctamente pero la respuesta se perdió o llegó tarde — sí hubo cobro. Como Pedidos no puede distinguirlos, el reintento solo es seguro si
cobrares idempotente: se reenvía con la misma clave de idempotencia (id del pedido) y Pagos devuelve el resultado ya registrado si el cobro existía. - (a) Cola: cada comanda debe procesarla exactamente un consumidor; las instancias compiten y además nivelan la carga. (b) Topic pub-sub: un mismo hecho interesa a varios suscriptores independientes y mañana puede añadirse otro sin tocar al emisor — Observer a escala de red. (c) Cola: trabajo a repartir con procesamiento individual y posibilidad de DLQ para reembolsos que fallen repetidamente.
confirmarPedido: abrir transacción →repositorioPedidos.guardar(pedido)→outbox.insertar(new EventoPedidoConfirmado(idEvento, idPedido))→ commit (ambas escrituras son atómicas por ser la misma BD). Relay (bucle o CDC): leer filas pendientes deoutbox→ publicar en el broker → marcar como enviadas; si se cae tras publicar y antes de marcar, al reiniciar vuelve a publicar. Resultado: at-least-once — el consumidor debe deduplicar poridEvento(registrando los procesados en la misma transacción que su efecto).
Conclusión
Hemos recorrido la fontanería que sostiene a la PideYa distribuida: el proxy remoto que disfraza la red de método (y las ocho falacias que castigan a quien se lo cree), las colas que desacoplan en el tiempo, el pub-sub que lleva la intención del Observer a escala planetaria, las garantías de entrega con su peaje de idempotencia, el Outbox que ata BD y broker, y la consistencia eventual que CAP hace inevitable. Todo esto trata de coordinar procesos separados por una red. Pero queda un frente más cercano y más traicionero: dentro de cada uno de esos servicios hay hilos compartiendo memoria, compitiendo por el mismo estado a nanosegundos de distancia. Aquel synchronized y aquel double-checked locking que pasamos de puntillas en el Singleton por fin van a explicarse de verdad: Patrones de Concurrencia.
Curso de Patrones de Diseño de Software
Módulo 1: Introducción a los Patrones de Diseño
- ¿Qué son los Patrones de Diseño?
- Historia y Origen de los Patrones de Diseño
- Principios de Diseño: SOLID y Otros Fundamentos
- UML Esencial para Entender Patrones
- Clasificación de los Patrones de Diseño
- Ventajas y Desventajas de Usar Patrones de Diseño
Módulo 2: Patrones Creacionales
- Introducción a los Patrones Creacionales
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparativa y Elección de Patrones Creacionales
Módulo 3: Patrones Estructurales
- Introducción a los Patrones Estructurales
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparativa y Elección de Patrones Estructurales
Módulo 4: Patrones de Comportamiento
- Introducción a los Patrones de Comportamiento
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparativa y Elección de Patrones de Comportamiento
Módulo 5: Aplicación de Patrones de Diseño
- Cómo Seleccionar el Patrón Adecuado
- Ejemplos Prácticos de Uso de Patrones
- Patrones de Diseño en Proyectos Reales
- Refactorización Usando Patrones de Diseño
- Antipatrones: Cuándo los Patrones se Vuelven un Problema
Módulo 6: Patrones de Diseño Avanzados
- Patrones de Diseño en Arquitecturas Modernas
- Patrones de Diseño en Microservicios
- Patrones de Diseño en Sistemas Distribuidos
- Patrones de Concurrencia
- Patrones de Diseño en Desarrollo Ágil
