Skip to content
SP StackPractices
intermediate Por StackPractices

Guía Completa de Comunicación entre Microservicios

Compara patrones de comunicación síncrona vs asíncrona para microservicios. Cubre REST, gRPC, colas de mensajes, event-driven, service mesh y cuándo usar cada uno.

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.

Guía Completa de Comunicación entre Microservicios

Introducción

Los microservicios deben comunicarse para entregar funcionalidad de negocio. La elección del patrón de comunicación afecta directamente latencia, confiabilidad, escalabilidad y acoplamiento. Esta guía cubre patrones síncronos (REST, gRPC), patrones asíncronos (colas de mensajes, event-driven), y patrones de infraestructura (service mesh, API gateway), con ejemplos de código prácticos y criterios de decisión.

Síncrono vs Asíncrono

AspectoSíncronoAsíncrono
AcoplamientoEstrecho (caller conoce callee)Loose (caller no conoce callee)
LatenciaCaller espera respuestaCaller continúa inmediatamente
FalloCaller falla si callee está downEl mensaje persiste, callee procesa después
EscalabilidadLimitada por el servicio más lentoMejor — servicios escalan independientemente
ComplejidadMás simple de implementarRequiere broker, idempotency, ordering
Caso de UsoRead-heavy, baja latenciaWrite-heavy, workflows desacoplados

Patrones Síncronos

REST (HTTP/JSON)

from fastapi import FastAPI, HTTPException
import httpx

app = FastAPI()

ORDER_SERVICE = "http://order-service:8000"
PAYMENT_SERVICE = "http://payment-service:8000"

@app.get("/orders/{order_id}/summary")
async def order_summary(order_id: str):
    async with httpx.AsyncClient(timeout=5.0) as client:
        try:
            order = await client.get(f"{ORDER_SERVICE}/orders/{order_id}")
            order.raise_for_status()
            payment = await client.get(f"{PAYMENT_SERVICE}/payments/{order_id}")
            payment.raise_for_status()
        except httpx.HTTPError as e:
            raise HTTPException(status_code=502, detail=f"Upstream error: {e}")

    return {
        "order": order.json(),
        "payment": payment.json(),
    }

Cuándo usar REST:

  • APIs públicas, integraciones externas
  • Operaciones CRUD
  • Payloads human-readable
  • Endpoints para browser

gRPC (HTTP/2 + Protobuf)

syntax = "proto3";

service OrderService {
  rpc GetOrder (OrderRequest) returns (OrderResponse);
  rpc CreateOrder (CreateOrderRequest) returns (OrderResponse);
}

message OrderRequest {
  string order_id = 1;
}

message OrderResponse {
  string order_id = 1;
  string status = 2;
  double total = 3;
}
import grpc
import order_pb2
import order_pb2_grpc

def get_order(order_id: str) -> order_pb2.OrderResponse:
    with grpc.insecure_channel("order-service:50051") as channel:
        stub = order_pb2_grpc.OrderServiceStub(channel)
        return stub.GetOrder(order_pb2.OrderRequest(order_id=order_id))

Cuándo usar gRPC:

  • Comunicación interna service-to-service
  • Requisitos de high-throughput, baja latencia
  • Strong typing across lenguajes
  • Streaming (bi-directional, server-streaming)

Patrones Asíncronos

Cola de Mensajes (Point-to-Point)

import pika
import json

connection = pika.BlockingConnection(pika.ConnectionParameters("rabbitmq"))
channel = connection.channel()
channel.queue_declare(queue="order_created", durable=True)

# Productor — Order service publica un mensaje
def publish_order_created(order_id: str, customer_id: str):
    message = json.dumps({"order_id": order_id, "customer_id": customer_id})
    channel.basic_publish(
        exchange="",
        routing_key="order_created",
        body=message,
        properties=pika.BasicProperties(delivery_mode=2),  # persistente
    )

# Consumidor — Shipping service procesa el mensaje
def consume_orders():
    def callback(ch, method, properties, body):
        order = json.loads(body)
        print(f"Shipping order {order['order_id']}")
        ch.basic_ack(delivery_tag=method.delivery_tag)

    channel.basic_consume(queue="order_created", on_message_callback=callback)
    channel.start_consuming()

Event-Driven (Pub/Sub)

const { Kafka } = require("kafkajs");

const kafka = new Kafka({
    clientId: "order-service",
    brokers: ["kafka:9092"],
});

const producer = kafka.producer();
const consumer = kafka.consumer({ groupId: "inventory-group" });

// Productor — publica eventos de dominio
async function publishOrderCreated(order) {
    await producer.connect();
    await producer.send({
        topic: "order.created",
        messages: [
            {
                key: order.id,
                value: JSON.stringify({
                    orderId: order.id,
                    customerId: order.customerId,
                    items: order.items,
                    timestamp: Date.now(),
                }),
            },
        ],
    });
    await producer.disconnect();
}

// Consumidor — múltiples servicios se suscriben al mismo evento
async function consumeOrderEvents() {
    await consumer.connect();
    await consumer.subscribe({ topic: "order.created", fromBeginning: false });

    await consumer.run({
        eachMessage: async ({ topic, partition, message }) => {
            const event = JSON.parse(message.value.toString());
            console.log(`Reserving inventory for order ${event.orderId}`);
            // Actualizar inventario, luego publicar evento inventory.reserved
        },
    });
}

consumeOrderEvents();

Event-Driven con Outbox Pattern (Java)

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.jdbc.core.JdbcTemplate;

@Service
public class OrderService {

    private final JdbcTemplate jdbc;

    public OrderService(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    @Transactional
    public void createOrder(Order order) {
        // 1. Guardar order
        jdbc.update(
            "INSERT INTO orders (id, customer_id, total) VALUES (?, ?, ?)",
            order.getId(), order.getCustomerId(), order.getTotal()
        );

        // 2. Guardar outbox event en la misma transacción
        jdbc.update(
            "INSERT INTO outbox (aggregate_id, event_type, payload) VALUES (?, ?, ?)",
            order.getId(), "OrderCreated", order.toJson()
        );
    }
}

// Proceso separado lee outbox y publica a Kafka
@Service
public class OutboxPublisher {

    private final JdbcTemplate jdbc;
    private final KafkaTemplate<String, String> kafka;

    public OutboxPublisher(JdbcTemplate jdbc, KafkaTemplate<String, String> kafka) {
        this.jdbc = jdbc;
        this.kafka = kafka;
    }

    @Scheduled(fixedDelay = 1000)
    public void publishPendingEvents() {
        var events = jdbc.queryForList(
            "SELECT id, aggregate_id, event_type, payload FROM outbox WHERE published = false LIMIT 100"
        );

        for (var event : events) {
            kafka.send("order." + event.get("event_type"),
                       (String) event.get("aggregate_id"),
                       (String) event.get("payload"));
            jdbc.update("UPDATE outbox SET published = true WHERE id = ?", event.get("id"));
        }
    }
}

Patrones de Infraestructura

API Gateway

# Configuración de Kong o NGINX API Gateway
apiVersion: v1
kind: ConfigMap
metadata:
  name: gateway-routes
data:
  kong.yml: |
    routes:
      - name: order-service
        paths:
          - /orders
        service:
          name: order-service
          url: http://order-service:8000
      - name: payment-service
        paths:
          - /payments
        service:
          name: payment-service
          url: http://payment-service:8000
    plugins:
      - name: rate-limiting
        config:
          minute: 100
      - name: jwt

Service Mesh (Istio)

apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: order-service
spec:
  hosts:
    - order-service
  http:
    - route:
        - destination:
            host: order-service
            subset: v1
            port:
              number: 8000
          weight: 90
        - destination:
            host: order-service
            subset: v2
            port:
              number: 8000
          weight: 10
---
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: order-service
spec:
  host: order-service
  subsets:
    - name: v1
      labels:
        version: v1
    - name: v2
      labels:
        version: v2
  trafficPolicy:
    outlierDetection:
      consecutive5xxErrors: 5
      interval: 30s
      baseEjectionTime: 30s

Framework de Decisión

NecesidadPatrónProtocolo
API públicaRESTHTTP/JSON
Alto rendimiento internogRPCHTTP/2 + Protobuf
Fire-and-forgetCola de MensajesAMQP/Kafka
Múltiples consumidoresPub/SubKafka/NATS
Desacoplamiento cross-teamEvent-DrivenKafka + Outbox
Control de tráficoAPI GatewayHTTP + plugins
mTLS, retries, tracingService MeshIstio/Linkerd
Agregación de peticionesGraphQLHTTP/JSON + schema

Pautas

  • Preferir async para workflows write-heavy — desacopla servicios, mejora resiliencia
  • Usar el Outbox pattern — asegura que eventos se publiquen exactly once con la transacción de DB
  • Hacer consumidores idempotentes — los mensajes pueden entregarse más de una vez
  • Setear timeouts en calls síncronas — nunca dejar que un caller hanguee indefinidamente
  • Usar circuit breakers para calls síncronas — fail fast cuando un servicio downstream está down
  • Versionar eventos — los consumidores pueden no upgradearse simultáneamente
  • Usar dead-letter queues — mensajes que fallan procesamiento van a DLQ para investigación
  • Monitorear latencia end-to-end — pipelines async pueden acumular latencia across hops
  • Mantener eventos pequeños — usar el Claim Check pattern para payloads grandes
  • Usar schema registry — enforce compatibilidad de schema de eventos (Avro, Protobuf)

Errores Comunes

  • Usar REST para todo — acoplamiento estrecho, fallos en cascada
  • No manejar mensajes duplicados — idempotency es obligatorio para consumidores async
  • Encadenar calls síncronas profundamente — la latencia se acumula, la probabilidad de fallo sube
  • No usar el Outbox pattern — dual-write a DB + broker no es atómico
  • Ignorar ordering de mensajes — algunos eventos deben procesarse en orden (e.g., order created antes que order cancelled)
  • No setear límites de concurrencia del consumidor — un consumidor lento puede exhaustar recursos
  • Mezclar sync y async para la misma operación — elegir un patrón por workflow
  • No monitorear queue depth — queues que crecen indican consumer lag

Preguntas Frecuentes

¿Debo usar REST o gRPC para comunicación interna?

Usar gRPC para calls internas service-to-service donde el performance importa. Ofrece menor latencia, payloads más pequeños y strong typing. Usar REST para APIs públicas, endpoints para browser e integraciones donde la interoperabilidad HTTP/JSON es requerida.

¿Cuál es la diferencia entre una cola de mensajes y pub/sub?

En una cola de mensajes (point-to-point), cada mensaje es consumido por exactamente un consumidor. En pub/sub, cada mensaje es entregado a todos los suscriptores. Usar queues para distribución de tareas (e.g., procesamiento de órdenes). Usar pub/sub para eventos de dominio (e.g., order created — inventory, shipping y analytics necesitan saber).

¿Necesito un service mesh?

Un service mesh es útil cuando tienes muchos microservicios (10+) y necesitas mTLS consistente, traffic splitting, retries y observabilidad sin modificar código de aplicación. Para menos servicios, librerías como resilience4j o Polly pueden manejar retries y circuit breaking in-process.