Skip to content
SP StackPractices
advanced Por StackPractices

Microservicios Event-Driven

Diseña microservicios event-driven con message brokers, event sourcing, CQRS y patrones de consistencia eventual.

Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.

Visión General

Los microservicios event-driven se comunican de forma asíncrona a través de eventos en lugar de llamadas directas a API. Esto desacopla servicios, mejora la resiliencia y permite escalado independiente. Patrones como event sourcing, CQRS, orquestación de sagas y el outbox pattern resuelven desafíos comunes: consistencia de datos, ordenamiento de mensajes, manejo de duplicados y recuperación de fallas.

Cuándo Usar

Usa este recurso cuando:

  • Los servicios necesitan escalar independientemente sin acoplamiento fuerte
  • Manejas procesos de negocio de larga duración a través de múltiples dominios
  • Aseguras consistencia de datos sin transacciones distribuidas
  • Construyes pipelines de notificaciones, auditoría o analytics en tiempo real

Solución

Event Sourcing con PostgreSQL (Python)

from dataclasses import dataclass
from typing import List
import json

@dataclass
class Event:
    aggregate_id: str
    event_type: str
    payload: dict
    version: int

class OrderAggregate:
    def __init__(self, order_id: str):
        self.order_id = order_id
        self.events: List[Event] = []
        self.status = "pending"
    
    def apply(self, event: Event):
        if event.event_type == "order_placed":
            self.status = "placed"
        elif event.event_type == "payment_received":
            self.status = "paid"
        self.events.append(event)
    
    def place_order(self, items: List[dict]):
        event = Event(
            aggregate_id=self.order_id,
            event_type="order_placed",
            payload={"items": items},
            version=len(self.events) + 1
        )
        self.apply(event)
        return event

Outbox Pattern (Node.js + Kafka)

// Dentro de la misma transacción de base de datos:
await db.transaction(async (trx) => {
  // 1. Actualizar datos de negocio
  await trx('orders').insert({ id: orderId, status: 'placed' });
  
  // 2. Escribir en tabla outbox (misma transacción)
  await trx('outbox').insert({
    topic: 'orders.events',
    key: orderId,
    payload: JSON.stringify({ event: 'order_placed', orderId, items })
  });
});

// Proceso relay separado hace polling de outbox y publica a Kafka
const pending = await db('outbox').where('sent', false).limit(100);
for (const msg of pending) {
  await kafka.producer.send({
    topic: msg.topic,
    messages: [{ key: msg.key, value: msg.payload }]
  });
  await db('outbox').where('id', msg.id).update({ sent: true });
}

Orquestación de Saga (TypeScript)

interface SagaStep {
  name: string;
  execute: () => Promise<void>;
  compensate: () => Promise<void>;
}

class OrderSaga {
  private steps: SagaStep[] = [
    {
      name: 'reserve_inventory',
      execute: () => inventoryService.reserve(order.items),
      compensate: () => inventoryService.release(order.items)
    },
    {
      name: 'process_payment',
      execute: () => paymentService.charge(order.total),
      compensate: () => paymentService.refund(order.total)
    },
    {
      name: 'ship_order',
      execute: () => shippingService.createLabel(order),
      compensate: () => shippingService.cancelLabel(order)
    }
  ];
  
  async execute() {
    const completed: SagaStep[] = [];
    try {
      for (const step of this.steps) {
        await step.execute();
        completed.push(step);
      }
    } catch (err) {
      // Rollback de pasos completados en orden inverso
      for (const step of completed.reverse()) {
        await step.compensate();
      }
      throw new Error(`Saga falló en paso ${completed[0]?.name}`);
    }
  }
}

Explicación

Patrones core:

PatrónProblema ResueltoCompromiso
Event SourcingAudit trail; queries temporalesComplejo; requiere CQRS para reads
CQRSOptimiza modelos de lectura/escritura separadosConsistencia eventual; más código
SagaTransacciones distribuidas sin locksRollback complejo; consistencia eventual
OutboxAtómico “DB update + publicación de mensaje”Requiere proceso relay
Idempotent ConsumerManejar mensajes duplicadosRequiere claves únicas por mensaje

Garantías de ordenamiento de mensajes:

  • Kafka: Ordenado por partition key (ej. order_id)
  • RabbitMQ: Ordenado por cola pero no entre consumers
  • SQS: Sin ordenamiento (usa FIFO queues para ordenamiento)

Variantes

BrokerOrdenamientoDeliveryIdeal Para
KafkaPor particiónAt-least-onceAlto throughput; replayability
RabbitMQPor colaAt-least-onceRouting complejo; colas prioritarias
NATSPor subjectAt-most-onceBaja latencia; simplicidad
PulsarGlobalExactly-onceGeo-replicación; tiered storage

Mejores Prácticas

  • Diseña eventos como hechos, no comandos: “OrderPlaced” no “PlaceOrder”
  • Incluye versiones de schema: Eventos V1 deben ser legibles por consumers V2
  • Maneja duplicados gracefulmente: Haz consumers idempotentes (upsert, no insert)
  • Monitorea dead letter queues: Mensajes fallidos necesitan investigación, no dropping silencioso
  • Mantén payloads de eventos pequeños: Referencia datos grandes; no embebas blobs

Errores Comunes

  1. Spaghetti event-driven: 50 microservicios suscritos al mismo evento crean acoplamiento invisible
  2. Idempotencia faltante: Procesar el mismo evento de pago dos veces cobra al cliente dos veces
  3. Cadenas síncronas de eventos: Llamar APIs HTTP dentro de event handlers anula el propósito
  4. Sin manejo de dead letter: Mensajes fallidos desaparecen; pierdes eventos de negocio
  5. Suposiciones incorrectas de ordenamiento: Asumir ordenamiento global cuando solo existe por partición

Preguntas Frecuentes

P: ¿Cuándo debo usar event sourcing vs. CRUD tradicional? R: Usa event sourcing para dominios donde el historial de auditoría, queries temporales o replay son críticos (finanzas, logística). Usa CRUD para dominios simples de CRUD.

P: ¿Cómo manejo evolución de schema en eventos? R: Usa schema registries (Confluent, AWS Glue). Agrega campos; nunca elimines. Mantén compatibilidad hacia atrás por 2+ versiones.

P: ¿Cuál es la diferencia entre sagas de coreografía y orquestación? R: Coreografía: los servicios reaccionan a eventos independientemente. Orquestación: un coordinador central dirige cada paso. La orquestación es más fácil de debug; la coreografía está más desacoplada.