Una arquitectura de microservicios solo resulta efectiva si los servicios colaboran mediante canales robustos, predecibles y bien observados. La comunicación define la latencia percibida por los usuarios, la consistencia de los datos y la capacidad de los equipos para desplegar de manera independiente. En este tema revisamos los enfoques sincrónicos y asincrónicos, patrones de mensajería y técnicas de tolerancia a fallos que permiten construir plataformas resilientes.
La comunicación sincrónica implica que el consumidor espera la respuesta del proveedor en la misma interacción. Este modelo es apropiado cuando se requiere consistencia inmediata o una respuesta rápida para el usuario final. Los microservicios suelen apoyarse en HTTP para intercambiar mensajes basados en texto, con formatos como JSON, y en REST para definir contratos uniformes. Cuando se necesita mayor eficiencia binaria y contratos estrictos, frameworks como gRPC ofrecen stubs generados a partir de schemas con Protocol Buffers.
El diseño de APIs sincrónicas debe priorizar la estabilidad de contratos, la versionación y la observabilidad. Es recomendable publicar los esquemas mediante OpenAPI, documentar los códigos de estado y adoptar un gateway que unifique autenticación, control de tráfico y métricas.
En el siguiente fragmento un servicio de pagos invoca a un servicio de pedidos utilizando el cliente reactivo WebClient de Spring. Se gestionan los tiempos de espera y se propaga un identificador de correlación para facilitar la trazabilidad.
@Service
public class OrderGateway {
private final WebClient webClient;
public OrderGateway(WebClient.Builder builder) {
this.webClient = builder
.baseUrl("http://orders-service.internal/api")
.build();
}
public Mono<OrderDto> getOrder(String orderId, String traceId) {
return webClient.get()
.uri("/orders/{id}", orderId)
.header("X-Correlation-Id", traceId)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, response ->
Mono.error(new OrderNotFoundException(orderId)))
.bodyToMono(OrderDto.class)
.timeout(Duration.ofSeconds(2));
}
}
Este enfoque es adecuado cuando la lógica de negocio depende de datos inmediatos. Si el servicio remoto sufre latencia elevada o indisponibilidad, debemos establecer fallbacks y reintentos controlados para proteger la experiencia del usuario.
También es posible utilizar un contrato binario compacto. En el archivo order.proto se define el servicio remoto y el siguiente código muestra cómo invocarlo desde un cliente basado en Protocol Buffers.
public class GrpcOrderClient {
private final ManagedChannel channel;
private final OrderServiceGrpc.OrderServiceBlockingStub stub;
public GrpcOrderClient(String host, int port) {
this.channel = ManagedChannelBuilder.forAddress(host, port)
.usePlaintext()
.build();
this.stub = OrderServiceGrpc.newBlockingStub(channel)
.withDeadlineAfter(1500, TimeUnit.MILLISECONDS);
}
public OrderReply fetchOrder(String id) {
OrderRequest request = OrderRequest.newBuilder()
.setOrderId(id)
.build();
return stub.getOrder(request);
}
public void shutdown() {
channel.shutdown();
}
}
Con gRPC se gana eficiencia en redes con alto volumen y se soportan canales streaming bidireccionales. La contrapartida es gestionar el balanceo de carga a través de service discovery y monitorear los tiempos de serialización.
La comunicación asincrónica desacopla el ciclo de vida de productores y consumidores mediante colas o buses de eventos. El publicador emite un mensaje y continua con su flujo sin esperar respuesta inmediata, mientras que los consumidores procesan el evento cuando están disponibles. Este paradigma reduce la latencia percibida por el usuario y tolera mejor los picos de carga.
Para implementarlo es habitual adoptar RabbitMQ cuando se requieren colas clásicas, confirmaciones individuales y ruteo flexible. Cuando prima la distribución de eventos de dominio y el procesamiento en streaming, plataformas como Apache Kafka ofrecen particiones escalables y almacenamiento persistente.
Un servicio de inventario podría publicar eventos de cambio de stock y otros servicios, como el carrito de compras, reaccionar a la información para validar reservas. El siguiente ejemplo muestra cómo publicar un evento utilizando Spring Cloud Stream y manejarlo desde otro servicio.
@Component
public class InventoryPublisher {
private final StreamBridge bridge;
public InventoryPublisher(StreamBridge bridge) {
this.bridge = bridge;
}
public void publishStockChanged(StockChanged event) {
bridge.send("stockChanged-out-0", MessageBuilder
.withPayload(event)
.setHeader("eventType", "StockChanged")
.build());
}
}
@Component
public class CartEventHandler {
@Bean
public Consumer<StockChanged> stockChangedListener() {
return event -> {
if (event.availableQuantity() == 0) {
cartService.markItemsAsUnavailable(event.sku());
}
};
}
}
Los eventos permiten notificar cambios de estado sin bloquear el flujo principal. Es importante garantizar que los mensajes se serialicen de forma compatible y contar con dead-letter queues para mensajes que no puedan ser procesados.
Los patrones de mensajería brindan vocabulario común para diseñar sistemas distribuidos:
Independientemente del patrón, debemos definir contratos para los mensajes, emplear esquemas versionados y controlar el volumen de eventos para prevenir backpressure.
El acoplamiento temporal entre servicios impone el riesgo de fallas en cadena. Para mitigarlo es vital aplicar estrategias como reintentos exponenciales, circuit breakers, timeouts ajustados y patrones idempotentes. Herramientas como Resilience4j simplifican la configuración de estas políticas.
Los reintentos deben aplicarse con moderación: repetir un comando de pago sin idempotencia puede propagar cobros duplicados. Los comandos deben incluir identificadores únicos y verificaciones de estado. Cuando una operación involucra varios servicios, las Sagas coordinan pasos con compensaciones para revertir acciones parciales.
Para la mensajería asincrónica, conviene habilitar colas de mensajes fallidos y mecanismos de retry diferenciados. En Kafka se acostumbra usar tópicos de retry con retardo controlado, mientras que en RabbitMQ las dead-letter exchanges permiten reenviar mensajes tras uno o más intentos.
El siguiente código envuelve una llamada remota en una configuración de reintentos y circuit breaker. La configuración se declara mediante anotaciones, dejando la política parametrizable.
@Service
public class PaymentGateway {
private final OrderGateway orderGateway;
public PaymentGateway(OrderGateway orderGateway) {
this.orderGateway = orderGateway;
}
@Retry(name = "orders")
@CircuitBreaker(name = "orders", fallbackMethod = "fallbackOrder")
public OrderDto fetchOrder(String orderId, String traceId) {
return orderGateway.getOrder(orderId, traceId)
.blockOptional()
.orElseThrow(() -> new IllegalStateException("Order unavailable"));
}
private OrderDto fallbackOrder(String orderId, String traceId, Throwable throwable) {
return new OrderDto(orderId, OrderStatus.UNKNOWN, traceId);
}
}
En la práctica se complementa la protección con alertas automatizadas, trazabilidad distribuida y tableros de métricas que permitan detectar anomalías antes de que impacten al negocio.