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:
- Debas elegir entre comunicación síncrona (REST, gRPC) y asíncrona (messaging, event-driven).
- Diseñes comunicación resiliente con circuit breakers y reintentos.
- Coordines transacciones distribuidas con sagas.
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
| Estilo | Protocolo | Mejor Para | Compromisos |
|---|---|---|---|
| REST | HTTP/JSON | Uso general, clientes de navegador, APIs públicas | Mayor latencia, contratos flexibles |
| gRPC | HTTP/2 + Protobuf | Servicio a servicio interno, alto throughput | Requiere tooling, menos legible para humanos |
| Messaging | AMQP, SQS, Kafka | Jobs de fondo, nivelación de carga, desacoplamiento | Overhead del broker, consistencia eventual |
| Event-driven | Kafka, event bus | Múltiples consumidores, auditoría, flujos complejos | Evolución de esquemas de eventos, coordinación de consumidores |
| GraphQL | HTTP | Queries flexibles, clientes móviles | Complejidad del servidor, desafíos de caché |
Lo que funciona
- 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.
- 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.
- Haz que las llamadas a downstream sean idempotentes. Pasa un header
Idempotency-Keypara que las solicitudes duplicadas causadas por reintentos no produzcan efectos secundarios. - 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.
- 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
- 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.
- Reintentar sin idempotencia. Reintentar un POST puede crear pedidos, cargos o envíos duplicados.
- Ignorar el ordenamiento de mensajes. Kafka con múltiples particiones puede reordenar mensajes; usa mensajes con clave o idempotencia si el orden importa.
- 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.
- 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)
);
}
}
Recursos Relacionados
Arquitectura de Microservicios — Cuándo Usarla y Cuándo No
Guía práctica de microservicios: beneficios, trade-offs, patrones comunes y cuándo elegirlos sobre monolitos. Cubre estrategias de descomposición y complejidad operativa.
GuideDe Monolito a Microservicios — Estrategias de Migración
Guía práctica para descomponer monolitos: strangler fig, branch by abstraction y patrones de extracción incremental que reducen riesgo y preservan continuidad del negocio.
GuideGuía de Arquitectura de Software
Una guía para diseñar arquitectura de software: monolitos vs microservicios, arquitectura en capas, flujo de datos y criterios de selección de tecnología.
GuideGuía de Entrevistas de System Design: Conceptos Clave
Una guía práctica para entrevistas de system design: escalabilidad, bases de datos, caching, load balancing, microservicios y cómo estructurar tu respuesta.
GuideTeorema CAP y Trade-offs de Bases de Datos
Guía práctica del teorema CAP: consistencia, disponibilidad y tolerancia a particiones. Aprende a elegir los trade-offs correctos para tu aplicación.
RecipeRetry con Exponential Backoff
Implementa estrategias de retry resilientes con exponential backoff, jitter e integración de circuit breaker para recuperación de fallas transitorias.