Saltar a contenido

05 - Patrones Avanzados

Tabla de Contenidos


Microservicios

Estilo arquitectónico donde la aplicación se compone de servicios pequeños, independientes y desplegables de forma autónoma, cada uno ejecutándose en su propio proceso y comunicándose mediante mecanismos ligeros (HTTP/gRPC/mensajes).

Monolito vs Microservicios

graph TD
    subgraph "Monolito"
        M[Aplicación Única]
        M --> MDB[(Base de Datos<br/>Compartida)]
        M --- M1[Auth Module]
        M --- M2[Orders Module]
        M --- M3[Products Module]
        M --- M4[Payments Module]
    end

    subgraph "Microservicios"
        GW[API Gateway] --> A[Auth Service]
        GW --> O[Order Service]
        GW --> P[Product Service]
        GW --> PAY[Payment Service]
        A --> ADB[(Auth DB)]
        O --> ODB[(Order DB)]
        P --> PDB[(Product DB)]
        PAY --> PAYDB[(Payment DB)]
        O <-->|gRPC| P
        O <-->|Events| PAY
    end

Principios de Diseño

  1. Single Responsibility: Cada servicio hace una cosa bien
  2. Database per Service: Cada servicio tiene su propia base de datos (no comparten esquema)
  3. Decentralized Governance: Cada equipo elige su stack tecnológico
  4. Design for Failure: Asumir que las llamadas a otros servicios pueden fallar
  5. Smart Endpoints, Dumb Pipes: La lógica está en los servicios, no en el middleware

API Gateway

Punto de entrada único que maneja concerns transversales:

graph TD
    C[Clientes] --> GW[API Gateway]
    GW --> |Rate Limiting| GW
    GW --> |Autenticación| GW
    GW --> |Routing| GW
    GW --> |Request Aggregation| GW
    GW --> S1[Service A]
    GW --> S2[Service B]
    GW --> S3[Service C]

Funciones del API Gateway: - Autenticación y autorización centralizada - Rate limiting y throttling - Request routing y load balancing - Response aggregation (BFF - Backend for Frontend) - SSL termination - Logging y monitoring - Circuit breaking

Herramientas populares: Kong, AWS API Gateway, Nginx + Lua, Envoy, Traefik

Service Mesh

Capa de infraestructura dedicada a la comunicación service-to-service. Cada servicio tiene un proxy sidecar.

graph LR
    subgraph "Service A Pod"
        A[App A] <--> PA[Envoy Proxy<br/>Sidecar]
    end
    subgraph "Service B Pod"
        B[App B] <--> PB[Envoy Proxy<br/>Sidecar]
    end
    PA <-->|mTLS, Retry,<br/>Circuit Break| PB
    PA --> CP[Control Plane<br/>Istio / Linkerd]
    PB --> CP

Resuelve: mTLS entre servicios, observabilidad (traces distribuidos), retry/timeout policies, canary deployments.

Desafíos de Microservicios

Desafío Solución
Transacciones distribuidas Saga Pattern (coreografía u orquestación)
Consistencia de datos Event sourcing + eventual consistency
Debugging distribuido Distributed tracing (Jaeger, Zipkin, OpenTelemetry)
Service discovery Consul, etcd, Kubernetes DNS
Config management Consul KV, AWS Parameter Store, Vault
Despliegue complejo CI/CD por servicio, Docker + Kubernetes
Testing Contract testing (Pact), integration tests con containers

Cuándo NO Usar Microservicios

  • Equipos pequeños (< 5-8 devs)
  • Dominio no bien comprendido (primero construye un monolito modular)
  • Sin cultura DevOps madura
  • Requisitos de latencia ultra baja (cada hop de red suma latencia)

Regla de oro: Empieza con un monolito modular. Extrae microservicios cuando el dolor de escalar lo justifique.


Serverless

Modelo de ejecución donde el cloud provider gestiona la infraestructura completamente. El código se ejecuta en funciones efímeras activadas por eventos.

Arquitectura Serverless Típica

graph LR
    U[Usuario] --> APIGW[API Gateway]
    APIGW --> L1[Lambda:<br/>createOrder]
    APIGW --> L2[Lambda:<br/>getOrder]
    L1 --> DDB[(DynamoDB)]
    L1 --> SQS[SQS Queue]
    SQS --> L3[Lambda:<br/>processPayment]
    L3 --> SNS[SNS Topic]
    SNS --> L4[Lambda:<br/>sendEmail]
    L2 --> DDB

    S3[S3: Upload imagen] --> L5[Lambda:<br/>resizeImage]
    L5 --> S3OUT[S3: Thumbnails]

    CRON[EventBridge:<br/>cada 5 min] --> L6[Lambda:<br/>cleanupExpired]

FaaS: Function as a Service

Proveedor Servicio Runtime Max Memoria Max Concurrencia
AWS Lambda 15 min 10 GB 1000 (default)
Google Cloud Functions 9 min (2nd gen) 32 GB Ilimitada
Azure Functions Ilimitado (plan premium) 14 GB 200 (plan consumption)
Cloudflare Workers 30 seg (free) 128 MB Ilimitada

Cold Start

Cuando una función se invoca por primera vez (o después de inactividad), el proveedor debe:

  1. Provisionar un contenedor
  2. Descargar el código
  3. Inicializar el runtime
  4. Ejecutar la función
graph LR
    subgraph "Cold Start (500-3000ms)"
        A[Provisionar Container] --> B[Cargar Código] --> C[Init Runtime] --> D[Ejecutar Handler]
    end
    subgraph "Warm Start (1-10ms)"
        E[Ejecutar Handler]
    end

Mitigación del cold start: - Provisioned concurrency (AWS): Mantener N instancias warm - Runtime liviano: Python/Node.js arrancan más rápido que Java/C# - Reducir dependencias: Menos código = menor tiempo de carga - SnapStart (AWS Lambda Java): Snapshot de la JVM inicializada

Ventajas y Desventajas

Ventajas Desventajas
Zero ops (no gestionar servidores) Cold starts (latencia variable)
Pay per invocation (idle = $0) Vendor lock-in
Auto-scaling automático Límites de ejecución (15 min max)
Deployment simple (sube un ZIP) Debugging/testing local difícil
Ideal para event-driven Stateless obligatorio
Reducción de costos a baja escala Costoso a alta escala constante

Serverless vs Containers vs VMs

graph TD
    subgraph "Espectro de Abstracción"
        VM[VMs / Bare Metal<br/>Control total<br/>Más ops] --> CONT[Containers / K8s<br/>Control medio<br/>Ops medio]
        CONT --> SL[Serverless / FaaS<br/>Sin control infra<br/>Zero ops]
    end
Criterio Serverless Containers VMs
Startup time Segundos (cold) Segundos Minutos
Costo en idle $0 Pago continuo Pago continuo
Escala a cero Con KEDA/Knative No
Workloads largos No (15 min max)
Personalización Limitada Alta Total
Costo a escala Puede ser alto Eficiente Eficiente

Circuit Breaker

Patrón de estabilidad que previene que una falla en un servicio downstream se propague en cascada a todo el sistema. Inspirado en los circuit breakers eléctricos.

Estados

stateDiagram-v2
    [*] --> Closed
    Closed --> Open: Umbral de fallos<br/>alcanzado
    Open --> HalfOpen: Timeout expira<br/>(cooldown period)
    HalfOpen --> Closed: Request de prueba<br/>exitoso
    HalfOpen --> Open: Request de prueba<br/>falla

    note right of Closed
        Requests fluyen normalmente.
        Se cuentan los fallos.
    end note

    note right of Open
        Requests rechazados inmediatamente.
        Retorna fallback/error.
        No se hace request al servicio.
    end note

    note right of HalfOpen
        Se permite UN request de prueba.
        Si éxito → Closed.
        Si fallo → Open.
    end note

Configuración Típica

failure_threshold: 5        # Fallos para abrir el circuito
success_threshold: 3         # Éxitos en half-open para cerrar
timeout: 30s                 # Tiempo en estado Open antes de probar
request_timeout: 2s          # Timeout por request individual
monitoring_window: 60s       # Ventana para contar fallos

Ejemplo Práctico

sequenceDiagram
    participant OrderSvc
    participant CB as Circuit Breaker
    participant PaymentSvc

    Note over CB: Estado: CLOSED

    OrderSvc->>CB: cobrar()
    CB->>PaymentSvc: POST /charge
    PaymentSvc-->>CB: 200 OK
    CB-->>OrderSvc: Cobro exitoso

    OrderSvc->>CB: cobrar()
    CB->>PaymentSvc: POST /charge
    PaymentSvc-->>CB: 500 Error
    Note over CB: Fallo 1/5

    OrderSvc->>CB: cobrar()
    CB->>PaymentSvc: POST /charge
    PaymentSvc-->>CB: Timeout
    Note over CB: Fallo 2/5

    Note over CB: ... 3 fallos más ...
    Note over CB: Estado: OPEN

    OrderSvc->>CB: cobrar()
    CB-->>OrderSvc: Error inmediato (sin llamar a Payment)
    Note over CB: Retorna fallback

    Note over CB: 30s después → HALF-OPEN

    OrderSvc->>CB: cobrar()
    CB->>PaymentSvc: POST /charge (prueba)
    PaymentSvc-->>CB: 200 OK
    Note over CB: Estado: CLOSED

Patrones Complementarios

1. Retry con Exponential Backoff

Intento 1: inmediato
Intento 2: esperar 1s
Intento 3: esperar 2s
Intento 4: esperar 4s
Intento 5: esperar 8s + jitter aleatorio

El jitter (aleatoriedad) evita que todos los clientes reintenten al mismo tiempo (thundering herd).

2. Bulkhead (Mamparo)

Aislar recursos para que un servicio fallido no consuma todos los threads/conexiones.

graph TD
    subgraph "Sin Bulkhead"
        TP1[Thread Pool: 100 threads]
        TP1 --> SA[Service A: 90 threads ocupados - LENTO]
        TP1 --> SB[Service B: 10 threads - Afectado]
    end

    subgraph "Con Bulkhead"
        TP2[Pool A: 50 threads]
        TP3[Pool B: 50 threads]
        TP2 --> SA2[Service A: 50 threads - LENTO]
        TP3 --> SB2[Service B: 50 threads - No afectado]
    end

3. Fallback

Cuando el circuit breaker está abierto, proveer una respuesta degradada:

  • Cache stale ("últimos datos conocidos")
  • Valor por defecto
  • Servicio alternativo
  • Mensaje al usuario: "Servicio temporalmente no disponible"

Implementaciones

Lenguaje/Framework Librería
Java Resilience4j, Hystrix (deprecated)
Node.js opossum, cockatiel
Python pybreaker, tenacity (retry)
Go sony/gobreaker, hystrix-go
.NET Polly

CQRS

Command Query Responsibility Segregation. Separar los modelos de lectura y escritura de la aplicación.

El Problema

En una arquitectura tradicional, el mismo modelo y la misma base de datos se usan para leer y escribir. Esto genera tensión:

  • Las lecturas necesitan desnormalización para ser rápidas (joins son costosos)
  • Las escrituras necesitan normalización para garantizar integridad
  • Las lecturas son generalmente mucho más frecuentes que las escrituras (ratio 100:1 es común)

Arquitectura CQRS

graph TD
    subgraph "Commands (Escritura)"
        CMD[Command: CreateOrder] --> CH[Command Handler]
        CH --> WDB[(Write DB<br/>Normalizada<br/>PostgreSQL)]
    end

    WDB -->|Eventos de cambio<br/>CDC / Domain Events| SYNC[Sincronización]

    subgraph "Queries (Lectura)"
        Q[Query: GetOrderSummary] --> QH[Query Handler]
        QH --> RDB[(Read DB<br/>Desnormalizada<br/>Elasticsearch / Redis)]
    end

    SYNC --> RDB

Niveles de CQRS

Nivel 1: Separación en Código

Misma DB, pero modelos separados para lectura y escritura en el código.

WriteModel: Order { userId, items[], status, shippingAddress }
ReadModel:  OrderSummary { orderId, userName, totalItems, total, statusLabel }

Nivel 2: Bases de Datos Separadas

Write DB optimizada para escrituras (normalizada, ACID). Read DB optimizada para lecturas (desnormalizada, eventualmente consistente).

Nivel 3: CQRS + Event Sourcing

La write side almacena eventos. La read side se construye proyectando esos eventos.

graph LR
    subgraph "Write Side"
        CMD[Command] --> AGG[Aggregate]
        AGG --> ES[(Event Store<br/>Append-only)]
    end

    ES -->|OrderCreated<br/>ItemAdded<br/>OrderShipped| PROJ[Projector]

    subgraph "Read Side"
        PROJ --> V1[(View: Order List<br/>SQL Table)]
        PROJ --> V2[(View: Dashboard<br/>Redis Hash)]
        PROJ --> V3[(View: Search<br/>Elasticsearch)]
    end

    Q1[Query: Lista] --> V1
    Q2[Query: Dashboard] --> V2
    Q3[Query: Buscar] --> V3

Eventual Consistency

En CQRS con DBs separadas, hay un delay entre una escritura y su reflejo en la read DB. Esto se llama replication lag o eventual consistency.

Estrategias para manejarlo:

  1. Optimistic UI: El frontend muestra el cambio inmediatamente sin esperar confirmación del read model
  2. Read-your-writes consistency: Después de escribir, leer directamente de la write DB (bypass temporal de la read DB)
  3. Polling / Webhooks: El cliente consulta hasta que el read model se actualice
  4. Causal consistency: Incluir un version/timestamp para que el cliente sepa si está viendo datos actualizados

Cuándo Usar CQRS

Sí: - Alta disparidad entre volumen de lecturas y escrituras - Modelos de lectura complejos (dashboards, reportes, búsquedas) - Necesidad de escalar lecturas y escrituras independientemente - Domain-driven design con aggregates complejos

No: - Aplicaciones CRUD simples - Dominio con lógica de negocio mínima - Equipos pequeños sin experiencia en eventual consistency - Cuando la consistencia inmediata es un requerimiento duro

CQRS en la Práctica: Stack Común

Componente Opciones
Write DB PostgreSQL, MySQL
Event Bus Kafka, RabbitMQ, EventBridge
CDC Debezium (captura cambios del WAL de PG)
Read DB (búsqueda) Elasticsearch, OpenSearch
Read DB (cache/views) Redis, DynamoDB
Read DB (analytics) ClickHouse, BigQuery

Recursos Recomendados

  • Libro: Building Microservices - Sam Newman (2da edición)
  • Libro: Implementing Domain-Driven Design - Vaughn Vernon
  • Libro: Release It! - Michael Nygard (Circuit Breaker, Bulkhead)
  • Paper: "CQRS Documents" - Greg Young
  • Blog: Martin Fowler - "Microservices", "CircuitBreaker", "CQRS"
  • Video: GOTO Conference - "When To Use Microservices" - Sam Newman
  • Curso: AWS Serverless Workshop (gratis en GitHub)
  • Herramienta: Resilience4j docs (resilience4j.readme.io)