En la lección anterior ordenamos el monolito de PideYa por dentro: capas limpias, puertos, adaptadores, repositorios. Pero el éxito trae un problema nuevo: el equipo de reparto no puede desplegar sin coordinarse con el de pagos, un pico de tráfico en el catálogo obliga a escalar toda la aplicación, y cada release es un acontecimiento. La respuesta de la industria es partir el sistema en microservicios: procesos pequeños, desplegables por separado, que se comunican por red. Esa decisión no es gratis — cada llamada a método se convierte en una llamada remota que puede fallar — y por eso existe un catálogo propio de patrones. Como verás, muchos son viejos conocidos del GoF estirados sobre la red.
Contenido
- Cómo partir el monolito: bounded contexts
- API Gateway y Backend for Frontend
- Service Registry y Discovery
- Circuit Breaker
- Retry con backoff e idempotencia
- Saga: transacciones entre servicios
- Strangler Fig: migrar sin big bang
- Database per Service y sus consecuencias
Cómo partir el monolito: bounded contexts
El primer error posible es partir mal. Los cortes no se hacen por capas técnicas ("servicio de base de datos", "servicio de lógica") sino por capacidades de negocio. El Domain-Driven Design (DDD, Eric Evans) — que aquí solo mencionamos — llama bounded context a cada frontera dentro de la cual un modelo tiene un significado coherente. En PideYa, "plato" significa cosas distintas para el catálogo (foto, descripción, alérgenos) y para cocina (tiempo de preparación, estación): son contextos distintos.
El corte de PideYa queda así:
| Servicio | Responsabilidad | Patrones GoF que se lleva del monolito |
|---|---|---|
| Catálogo | Carta, precios, disponibilidad | Composite (carta), Flyweight, Visitor |
| Pedidos | Ciclo de vida del pedido | State, Builder, Chain de validación |
| Pagos | Cobros y reembolsos | Adapter/Abstract Factory de pasarelas |
| Reparto | Asignación y seguimiento | Mediator (CentralReparto), Strategy |
| Notificaciones | Email, SMS, push | Bridge, Decorator (reintentos, logging) |
Fíjate: cada servicio se lleva consigo los patrones internos que construimos en los módulos 2-4. Los microservicios cambian el entre, no el dentro.
API Gateway y Backend for Frontend
Con cinco servicios, ¿la app móvil debe conocer cinco URLs, cinco esquemas de autenticación, cinco formatos de error? No: se antepone un API Gateway, un único punto de entrada que enruta, autentica, limita tráfico y agrega respuestas.
¿Reconoces la intención? Es la Facade de 03-06 a escala de red: ocultar un subsistema complejo tras una interfaz simple. FachadaCheckout simplificaba llamadas entre objetos; el gateway simplifica llamadas entre procesos.
flowchart LR
APP[App cliente] --> GW[API Gateway]
WEB[Web] --> GW
RID[App repartidor] --> BFF[BFF repartidor]
GW --> CAT[Catálogo]
GW --> PED[Pedidos]
GW --> PAG[Pagos]
BFF --> PED
BFF --> REP[Reparto]
Cuando cada tipo de cliente necesita agregaciones muy distintas (la app del repartidor quiere rutas y pedidos asignados; la web del cliente quiere carta y seguimiento), un único gateway engorda hasta volverse un cuello de botella de equipo. El patrón Backend for Frontend (BFF) crea un gateway por tipo de cliente, mantenido por el equipo de ese frontend.
Service Registry y Discovery
En el monolito, llamar al módulo de pagos era pasarela.cobrar(...). Ahora Pagos son tres instancias cuyas IPs cambian con cada despliegue. El Service Registry (Eureka, Consul, o el DNS de Kubernetes) es un directorio donde cada instancia se registra al arrancar y del que los clientes obtienen direcciones frescas (discovery). Es la vieja idea del desacoplamiento por indirección: nadie referencia instancias concretas, igual que en Factory Method nadie referenciaba clases concretas — aquí la "fábrica" te fabrica direcciones.
Circuit Breaker
La llamada remota introduce un modo de fallo nuevo: el servicio lento. Si Pagos tarda 30 segundos en responder, cada checkout retiene un hilo 30 segundos, los hilos se agotan y Pedidos cae también: fallo en cascada. El Circuit Breaker (popularizado por Nussbaumer/Nygard en Release It!) actúa como un fusible:
stateDiagram-v2
[*] --> Closed
Closed --> Open : fallos >= umbral
Open --> HalfOpen : pasa el tiempo de espera
HalfOpen --> Closed : llamada de prueba OK
HalfOpen --> Open : llamada de prueba falla
Closed --> Closed : éxito (resetea contador)
¿Y esa máquina de estados no te recuerda a algo? Es el patrón State de 04-09 aplicado a la salud de una conexión. Una implementación simplificada:
public class CircuitBreaker {
private enum Estado { CLOSED, OPEN, HALF_OPEN }
private Estado estado = Estado.CLOSED;
private int fallosSeguidos = 0;
private long abiertoDesde = 0;
private final int umbralFallos; // p. ej. 5
private final long esperaMs; // p. ej. 10_000
public CircuitBreaker(int umbralFallos, long esperaMs) {
this.umbralFallos = umbralFallos;
this.esperaMs = esperaMs;
}
public synchronized <T> T ejecutar(Supplier<T> llamada, Supplier<T> fallback) {
if (estado == Estado.OPEN) {
if (System.currentTimeMillis() - abiertoDesde < esperaMs) {
return fallback.get(); // circuito abierto: ni lo intentamos
}
estado = Estado.HALF_OPEN; // dejamos pasar UNA llamada de prueba
}
try {
T resultado = llamada.get();
estado = Estado.CLOSED; // éxito: el servicio se ha recuperado
fallosSeguidos = 0;
return resultado;
} catch (Exception e) {
fallosSeguidos++;
if (estado == Estado.HALF_OPEN || fallosSeguidos >= umbralFallos) {
estado = Estado.OPEN;
abiertoDesde = System.currentTimeMillis();
}
return fallback.get();
}
}
}Puntos a entender del código: en OPEN fallamos rápido con un fallback (por ejemplo, "guardamos tu pedido y te cobraremos en unos minutos") en lugar de esperar un timeout; HALF_OPEN deja pasar una llamada-sonda para comprobar si el servicio revivió. En producción usarías Resilience4j, que envuelve la llamada igual que un Decorator (03-05) — de hecho su API se llama literalmente CircuitBreaker.decorateSupplier.
Retry con backoff e idempotencia
Muchos fallos de red son transitorios: reintentar suele bastar. Pero reintentar mal es peor que no reintentar:
- Backoff exponencial: espera 100 ms, luego 200, 400, 800... para no rematar a un servicio que se está recuperando. Añade jitter (aleatoriedad) para que mil clientes no reintenten sincronizados.
- Idempotencia: si reintentas
cobrar(pedido)porque no llegó la respuesta... ¿y si el primer cobro sí se ejecutó? Cobro doble. La solución es que la operación sea idempotente: ejecutarla dos veces tiene el efecto de una. En PideYa, cada cobro lleva una clave de idempotencia (el id del pedido); si Pagos recibe dos veces la misma clave, la segunda vez devuelve el resultado guardado sin cobrar de nuevo.
Regla práctica: nunca actives reintentos sobre una operación no idempotente. Este dúo reaparecerá con la mensajería en 06-03.
Saga: transacciones entre servicios
En el monolito, el checkout era una transacción de BD: pedido + cobro + stock, todo o nada (la Unit of Work de la lección anterior). Con Database per Service ya no hay transacción que abarque Pedidos, Pagos y Reparto. El patrón Saga la sustituye por una secuencia de transacciones locales, donde cada paso que falla dispara compensaciones que deshacen los pasos anteriores.
| Estilo | Cómo funciona | Recuerda a |
|---|---|---|
| Coreografía | Cada servicio escucha eventos y reacciona; nadie dirige | Observer encadenado; simple con pocos pasos, ilegible con muchos |
| Orquestación | Un orquestador de saga dice a cada servicio qué hacer y espera respuesta | Mediator (04-06): centraliza la conversación, como CentralReparto |
Saga orquestada del checkout de PideYa:
- Orquestador → Pedidos: crear pedido (local, confirmado).
- Orquestador → Pagos: cobrar. Si falla → compensar: Pedidos.cancelarPedido().
- Orquestador → Reparto: asignar repartidor. Si falla → compensar: Pagos.reembolsar(), Pedidos.cancelarPedido().
Las compensaciones son la intención del undo de Command (04-03) a escala de sistema: cada paso define su operación inversa. Ojo: no es un rollback mágico — el cobro ocurrió y el reembolso es una operación de negocio nueva, visible para el cliente.
Strangler Fig: migrar el monolito
PideYa no reescribe el monolito de golpe (el "big bang" casi siempre acaba mal). El patrón Strangler Fig (Fowler) — llamado así por la higuera estranguladora que crece rodeando un árbol hasta sustituirlo — consiste en:
- Poner una fachada de enrutado (el propio API Gateway sirve) delante del monolito.
- Extraer una capacidad (p. ej. Notificaciones, la más desacoplada gracias al Bridge de 03-03) a un servicio nuevo.
- Redirigir en el gateway ese tráfico al servicio nuevo; el resto sigue yendo al monolito.
- Repetir hasta que el monolito quede vacío... o hasta que deje de doler, que a veces llega antes.
Es la estrategia de refactorización de 05-04 elevada a arquitectura: pasos pequeños, sistema siempre funcionando, y los tests de caracterización ahora son tests de contrato entre servicios.
Database per Service
Cada servicio posee su base de datos y nadie más la toca; los demás acceden solo por su API. Sin esto, los microservicios son un monolito distribuido: la BD compartida acopla los esquemas y los despliegues.
Consecuencias que hay que aceptar (no son fallos, son el precio):
- No hay JOIN entre servicios: la pantalla "mis pedidos con nombres de platos" exige componer datos de Pedidos y Catálogo (en el gateway/BFF, o duplicando datos).
- No hay transacciones globales: de ahí las sagas.
- Consistencia eventual: los datos duplicados tardan en converger — lo trataremos a fondo en la siguiente lección.
- A cambio: cada servicio elige su almacenamiento (Catálogo puede usar un documental; Pagos, relacional estricto) y despliega su esquema sin coordinarse.
Errores Comunes y Consejos
- Empezar por microservicios: para un equipo pequeño, el monolito bien organizado de la lección 06-01 es más rápido y barato. Los microservicios resuelven problemas de escala organizativa (muchos equipos, despliegues independientes). "Espera hasta que duela" también aplica aquí.
- El monolito distribuido: servicios que comparten BD o que deben desplegarse juntos. Tienes todos los costes de la red y ninguna ventaja. Síntoma: una historia de usuario típica toca tres servicios.
- Nanoservicios: cortar demasiado fino (un servicio por entidad) multiplica llamadas de red y sagas. El corte correcto sigue a los bounded contexts, no a las tablas.
- Reintentos sin idempotencia: el clásico cobro duplicado. Diseña la clave de idempotencia antes de activar reintentos.
- Circuit breaker sin fallback pensado: abrir el circuito y devolver un error 500 pelado apenas mejora nada. El valor está en el plan B de negocio (degradar, encolar, responder con caché).
- Saga sin diseñar las compensaciones: si
reembolsar()no existe o puede fallar sin plan, la saga deja el sistema en un estado intermedio invisible. Las compensaciones son requisitos de negocio de primera clase.
Ejercicios
- Detecta el mal corte. Un arquitecto propone estos servicios para PideYa:
servicio-entidades(todas las clases de dominio),servicio-logica(todos los casos de uso) yservicio-datos(todo el acceso a BD). Explica por qué es un mal corte y qué criterio debería usarse. - Traza la saga. El cliente pide con un cupón de fidelización (módulo del caso práctico de 05-02). La saga es: crear pedido → canjear cupón (servicio Fidelización) → cobrar importe con descuento → asignar reparto. Enumera las compensaciones necesarias si falla (a) el cobro y (b) la asignación de reparto.
- Razona el circuit breaker. Con el
CircuitBreakerde la lección configurado conumbralFallos=3yesperaMs=10000: partiendo deCLOSED, llegan estas llamadas: fallo, fallo, éxito, fallo, fallo, fallo, (pasan 4 s) llamada, (pasan 7 s más) llamada con éxito. Indica el estado tras cada paso y qué recibe cada llamador.
Soluciones
- Es un corte por capas técnicas, no por capacidades de negocio: cualquier funcionalidad nueva ("añadir propinas") tocaría los tres servicios a la vez, obligando a desplegarlos coordinados — un monolito distribuido. El criterio correcto son los bounded contexts: fronteras dentro de las cuales un modelo es coherente y un equipo puede trabajar y desplegar de forma autónoma (catálogo, pedidos, pagos, reparto, notificaciones).
- (a) Falla el cobro: compensar en orden inverso → Fidelización.devolverCupon() y Pedidos.cancelarPedido(). (b) Falla el reparto: Pagos.reembolsar(), Fidelización.devolverCupon(), Pedidos.cancelarPedido(). Observa que las compensaciones se ejecutan en orden inverso al de los pasos, exactamente como una pila de undo de Command; y que "devolver un cupón ya canjeado" debe existir como operación de negocio.
- Fallo (1/3, CLOSED) → fallo (2/3, CLOSED) → éxito (contador a 0, CLOSED) → fallo (1/3) → fallo (2/3) → fallo (3/3 → OPEN, se anota el instante). A los 4 s: sigue en OPEN (4000 < 10000), el llamador recibe el fallback sin intentar la llamada. A los 11 s de la apertura: pasa a HALF_OPEN, se ejecuta la llamada de prueba, tiene éxito → CLOSED y contador a 0; el llamador recibe el resultado real.
Conclusión
PideYa ya es una plataforma: cinco servicios cortados por bounded contexts, un gateway que los esconde (Facade), un registro que los localiza, fusibles que aíslan sus fallos (State), reintentos idempotentes y sagas que sustituyen a las transacciones (Mediator + undo de Command), todo migrado gradualmente con Strangler Fig. Pero hemos dado por sentado lo más frágil: la comunicación misma. ¿Qué pasa cuando un mensaje se pierde, se duplica o llega tarde? ¿Cómo se propaga el evento "pedido confirmado" a cinco servicios sin llamarlos uno a uno? Ese es el territorio de las colas, los brokers y la consistencia eventual: Patrones de Diseño en Sistemas Distribuidos.
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
