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

  1. Cómo partir el monolito: bounded contexts
  2. API Gateway y Backend for Frontend
  3. Service Registry y Discovery
  4. Circuit Breaker
  5. Retry con backoff e idempotencia
  6. Saga: transacciones entre servicios
  7. Strangler Fig: migrar sin big bang
  8. 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:

  1. Orquestador → Pedidos: crear pedido (local, confirmado).
  2. Orquestador → Pagos: cobrar. Si falla → compensar: Pedidos.cancelarPedido().
  3. 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:

  1. Poner una fachada de enrutado (el propio API Gateway sirve) delante del monolito.
  2. Extraer una capacidad (p. ej. Notificaciones, la más desacoplada gracias al Bridge de 03-03) a un servicio nuevo.
  3. Redirigir en el gateway ese tráfico al servicio nuevo; el resto sigue yendo al monolito.
  4. 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

  1. 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) y servicio-datos (todo el acceso a BD). Explica por qué es un mal corte y qué criterio debería usarse.
  2. 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.
  3. Razona el circuit breaker. Con el CircuitBreaker de la lección configurado con umbralFallos=3 y esperaMs=10000: partiendo de CLOSED, 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

  1. 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).
  2. (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.
  3. 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

Módulo 2: Patrones Creacionales

Módulo 3: Patrones Estructurales

Módulo 4: Patrones de Comportamiento

Módulo 5: Aplicación de Patrones de Diseño

Módulo 6: Patrones de Diseño Avanzados

Módulo 7: Recursos Adicionales y Conclusión

© Copyright 2026. Todos los derechos reservados