StackPractices
advanced Por Mathias Paulenko

Patrones de Comunicación entre Microservicios

Elige entre patrones de comunicación síncronos y asíncronos para arquitecturas de microservicios resilientes.

Visión General

Los microservicios deben intercambiar datos para cumplir con las solicitudes de los usuarios, pero elegir el estilo de comunicación equivocado puede convertir un sistema distribuido en una red frágil y fuertemente acoplada. Cada interacción entre servicios es un punto potencial de fallo: picos de latencia, fallos parciales, particiones de red y errores en cascada pueden surgir de una sola dependencia lenta.

Esta receta compara los principales patrones de comunicación utilizados en microservicios productivos: llamadas síncronas REST y gRPC, mensajería asíncrona con brokers de mensajes, y arquitecturas orientadas a eventos. Aprenderás cuándo usar cada uno, cómo hacerlos resilientes con reintentos, timeouts, circuit breakers e idempotencia, y cómo coordinar transacciones de negocio de larga duración con sagas.

Cuándo Usar

Usa este recurso cuando:

Solución

Llamada REST síncrona

# Python con httpx
import httpx
from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
def get_order(order_id: str) -> dict:
    with httpx.Client(timeout=5.0) as client:
        r = client.get(f"http://orders-service/orders/{order_id}")
        r.raise_for_status()
        return r.json()
// JavaScript con fetch
async function getOrder(orderId) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 5000);
  const res = await fetch(`http://orders-service/orders/${orderId}`, {
    signal: controller.signal,
  });
  clearTimeout(timeout);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}
// Java con RestTemplate
import org.springframework.web.client.RestTemplate;

RestTemplate rest = new RestTemplate();
Order order = rest.getForObject(
  "http://orders-service/orders/{id}", Order.class, orderId);

Productor de mensajes asíncronos

# Python con RabbitMQ (pika)
import pika, json

def publish_order_created(order: dict):
    conn = pika.BlockingConnection(pika.ConnectionParameters('rabbitmq'))
    ch = conn.channel()
    ch.queue_declare(queue='orders.created')
    ch.basic_publish(
        exchange='',
        routing_key='orders.created',
        body=json.dumps(order).encode()
    )
    conn.close()
// Node.js con Kafka (kafkajs)
const { Kafka } = require('kafkajs');
const kafka = new Kafka({ brokers: ['kafka:9092'] });
const producer = kafka.producer();

async function publishOrderCreated(order) {
  await producer.connect();
  await producer.send({
    topic: 'orders.created',
    messages: [{ key: order.id, value: JSON.stringify(order) }],
  });
  await producer.disconnect();
}

Explicación

La comunicación síncrona es el modelo mental más simple: el servicio A llama al servicio B y espera una respuesta. REST sobre HTTP es la opción predeterminada porque es ubicuo, independiente del lenguaje y fácil de depurar. gRPC es más adecuado cuando importan la baja latencia, los payloads binarios y los contratos fuertemente tipados. El costo de las llamadas síncronas es el acoplamiento temporal: si el servicio downstream es lento o está caído, el llamador también se ve afectado.

La comunicación asíncrona desacopla los servicios introduciendo un broker de mensajes. El productor envía un mensaje y continúa inmediatamente; el consumidor lo procesa a su propio ritmo. Esto mejora la resiliencia y el throughput, pero añade complejidad operativa (clustering del broker, dead-letter queues, ordenamiento de mensajes) y dificulta la depuración porque no hay una única stack trace.

Las arquitecturas orientadas a eventos extienden la mensajería haciendo que los cambios de estado sean observables como eventos de dominio. Los consumidores se suscriben a los eventos relevantes, permitiendo capacidades de negocio débilmente acopladas. Usa este patrón cuando múltiples servicios deban reaccionar al mismo hecho sin conocerse entre sí.

Los patrones de resiliencia son obligatorios en cualquier estilo. Agrega timeouts del lado del cliente para dejar de esperar a un peer lento, reintentos con backoff exponencial y jitter para recuperarte de fallos transitorios, circuit breakers para fallar rápido cuando un servicio está inestable, y claves de idempotencia para hacer que los reintentos sean seguros.

Las sagas reemplazan a las transacciones distribuidas. Una saga es una secuencia de transacciones locales, cada una seguida de un evento o mensaje. Si un paso falla, las acciones compensatorias deshacen los pasos anteriores. Esto mantiene los servicios autónomos mientras se preserva la consistencia de negocio.

Variantes

EstiloProtocoloMejor ParaCompromisos
RESTHTTP/JSONUso general, clientes de navegador, APIs públicasMayor latencia, contratos flexibles
gRPCHTTP/2 + ProtobufServicio a servicio interno, alto throughputRequiere tooling, menos legible para humanos
MessagingAMQP, SQS, KafkaJobs de fondo, nivelación de carga, desacoplamientoOverhead del broker, consistencia eventual
Event-drivenKafka, event busMúltiples consumidores, auditoría, flujos complejosEvolución de esquemas de eventos, coordinación de consumidores
GraphQLHTTPQueries flexibles, clientes móvilesComplejidad del servidor, desafíos de caché

Lo que funciona

  1. Prefiere la comunicación asíncrona para operaciones de larga duración o no críticas. Usa mensajería o eventos cuando el llamador no necesita un resultado inmediato.
  2. Configura timeouts agresivos y presupuestos de reintento pequeños. Una tormenta de reintentos puede amplificar una interrupción parcial. Limita los reintentos a 3 intentos y usa backoff exponencial con jitter.
  3. Haz que las llamadas a downstream sean idempotentes. Pasa un header Idempotency-Key para que las solicitudes duplicadas causadas por reintentos no produzcan efectos secundarios.
  4. Despliega circuit breakers alrededor de cada dependencia externa. Abre el circuito tras un umbral de fallos y degrada gracefulmente en lugar de propagar el error.
  5. Mantén las compensaciones de sagas simples y reversibles. Cada paso de una saga debe tener una acción compensatoria clara que pueda ejecutarse en segundo plano.

Errores Comunes

  1. Encadenar llamadas síncronas a través de muchos servicios. Cada salto añade latencia y superficie de fallo; los grafos profundos de llamadas se vuelven frágiles.
  2. Reintentar sin idempotencia. Reintentar un POST puede crear pedidos, cargos o envíos duplicados.
  3. Ignorar el ordenamiento de mensajes. Kafka con múltiples particiones puede reordenar mensajes; usa mensajes con clave o idempotencia si el orden importa.
  4. Compartir una base de datos entre servicios. El acoplamiento directo a la base de datos anula el propósito de los microservicios y bloquea el despliegue independiente.
  5. Bloquear al llamador con un consumidor lento. Si el consumidor no puede seguir el ritmo, las colas crecen y los productores eventualmente sufren back-pressure o caídas.

Preguntas frecuentes

gRPC Servicio-a-Servicio (TypeScript)
import { credentials, makeClientConstructor } from '@grpc/grpc-js';
import { loadPackageDefinition } from '@grpc/grpc-js';
import { loadSync } from '@grpc/proto-loader';

const packageDefinition = loadSync('proto/orders.proto', {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true,
});

const protoDescriptor = loadPackageDefinition(packageDefinition);
const OrderServiceClient = makeClientConstructor(
  (protoDescriptor as any).orders.OrderService.service,
  'OrderService'
);

class OrderGrpcClient {
  private client: any;

  constructor(address: string = 'orders-service:50051') {
    this.client = new OrderServiceClient(address, credentials.createInsecure());
  }

  getOrder(orderId: string): Promise<Order> {
    return new Promise((resolve, reject) => {
      this.client.getOrder({ id: orderId }, (err: Error | null, response: Order) => {
        if (err) reject(err);
        else resolve(response);
      });
    });
  }

  createOrder(items: OrderItem[]): Promise<Order> {
    return new Promise((resolve, reject) => {
      this.client.createOrder({ items }, (err: Error | null, response: Order) => {
        if (err) reject(err);
        else resolve(response);
      });
    });
  }
}
Consumidor de Mensajes Asíncronos con Dead-Letter Queue (Python)
import pika
import json
import logging

logger = logging.getLogger(__name__)

class OrderConsumer:
    def __init__(self, rabbitmq_url: str = 'amqp://rabbitmq:5672'):
        self.connection = pika.BlockingConnection(
            pika.ConnectionParameters(rabbitmq_url)
        )
        self.channel = self.connection.channel()

        # Cola principal
        self.channel.queue_declare(queue='orders.created', durable=True)
        # Dead-letter queue para mensajes fallidos
        self.channel.queue_declare(queue='orders.created.dlq', durable=True)

    def process_message(self, ch, method, properties, body):
        try:
            order = json.loads(body)
            self._handle_order(order)
            ch.basic_ack(delivery_tag=method.delivery_tag)
        except Exception as e:
            logger.error(f'Failed to process order: {e}')
            # Rechazar y re-encolar hasta 3 veces, luego enviar a DLQ
            headers = properties.headers or {}
            retry_count = headers.get('x-retry-count', 0)
            if retry_count < 3:
                ch.basic_publish(
                    exchange='',
                    routing_key='orders.created',
                    body=body,
                    properties=pika.BasicProperties(
                        headers={'x-retry-count': retry_count + 1}
                    )
                )
            else:
                ch.basic_publish(
                    exchange='',
                    routing_key='orders.created.dlq',
                    body=body,
                    properties=pika.BasicProperties(
                        headers={'x-retry-count': retry_count + 1}
                    )
                )
            ch.basic_ack(delivery_tag=method.delivery_tag)

    def _handle_order(self, order: dict):
        logger.info(f'Processing order {order["id"]}')
        # Lógica de negocio aquí

    def start(self):
        self.channel.basic_consume(
            queue='orders.created',
            on_message_callback=self.process_message
        )
        logger.info('Waiting for orders...')
        self.channel.start_consuming()
Circuit Breaker con Resilience4j (Java)
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.retry.Retry;
import io.vavr.control.Try;

import java.time.Duration;

public class ResilientPaymentClient {
    private final CircuitBreaker circuitBreaker;
    private final Retry retry;
    private final PaymentGateway gateway;

    public ResilientPaymentClient(PaymentGateway gateway) {
        this.gateway = gateway;
        this.circuitBreaker = CircuitBreaker.of("payment",
            CircuitBreakerConfig.custom()
                .failureRateThreshold(50)
                .waitDurationInOpenState(Duration.ofSeconds(30))
                .slidingWindowSize(10)
                .minimumNumberOfCalls(5)
                .build()
        );
        this.retry = Retry.of("payment",
            RetryConfig.custom()
                .maxAttempts(3)
                .waitDuration(Duration.ofMillis(500))
                .build()
        );
    }

    public PaymentResult charge(PaymentRequest request) {
        return Try.of(() ->
            Retry.decorateSupplier(retry,
                CircuitBreaker.decorateSupplier(circuitBreaker,
                    () -> gateway.charge(request)
                )
            ).get()
        ).getOrElseThrow(throwable ->
            new PaymentFailedException("Payment service unavailable", throwable)
        );
    }
}