Comunicación síncrona vs asíncrona: qué problema resuelve cada una
En una arquitectura de microservicios, la comunicación define el acoplamiento, la latencia percibida y el comportamiento ante fallos. Hay dos estilos principales: síncrono (petición/respuesta, típicamente REST) y asíncrono (eventos/mensajería). La resiliencia no se “añade al final”: se diseña junto con el estilo de comunicación.
Comunicación síncrona (REST): cuándo usarla
- Lecturas y consultas donde el usuario necesita respuesta inmediata (p. ej., “ver detalle de pedido”).
- Orquestación simple cuando un servicio necesita datos de otro para completar una operación.
- Interacciones de baja complejidad y con contratos claros de request/response.
Coste principal: si el servicio remoto está lento o caído, el llamador puede bloquearse y provocar cascadas de fallos si no hay límites (timeouts) y control (circuit breaker).
Comunicación asíncrona (eventos): cuándo usarla
- Propagación de cambios (p. ej., “PedidoCreado”, “PagoConfirmado”).
- Integración desacoplada entre dominios: productores no conocen consumidores.
- Procesamiento eventual donde la respuesta inmediata no es necesaria.
- Escalabilidad y absorción de picos mediante colas/streams.
Coste principal: complejidad operativa y de consistencia (eventual), necesidad de idempotencia y trazabilidad (correlación) para depurar flujos.
Criterios prácticos para elegir
| Criterio | REST (síncrono) | Eventos (asíncrono) |
|---|---|---|
| Necesidad de respuesta inmediata | Alta | Baja/Media |
| Acoplamiento temporal (si el otro cae) | Alto | Bajo |
| Consistencia | Más inmediata | Eventual |
| Complejidad | Menor | Mayor (reintentos, duplicados, orden) |
| Observabilidad | Más directa | Requiere correlación fuerte |
Regla útil: comandos críticos para el usuario suelen empezar síncronos (validaciones, aceptación), pero la propagación y efectos secundarios (notificaciones, analítica, sincronización) suelen ir por eventos.
Resiliencia en llamadas REST: límites, reintentos y circuit breaker
Una llamada HTTP entre microservicios debe tener, como mínimo: timeouts (para no bloquear), reintentos (solo cuando sea seguro), y circuit breaker (para cortar llamadas a un dependiente degradado). Además, debe incluir correlación (traceId) para rastrear el flujo.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
Dependencias recomendadas (conceptual)
En Spring Boot, un enfoque común es: WebClient para HTTP no bloqueante y Resilience4j para patrones de resiliencia (retry, circuit breaker, bulkhead, rate limiter). Si tu servicio es MVC (bloqueante), puedes seguir usando WebClient, pero evita bloquear hilos sin control; si bloqueas, hazlo con límites claros.
Guía práctica: cliente HTTP con WebClient + timeouts
Paso 1: crear un WebClient con configuración base
Define un bean de WebClient con URL base del servicio remoto y timeouts a nivel de cliente. Un timeout típico incluye: conexión, lectura/escritura y un timeout global por request.
@Configuration
public class HttpClientConfig {
@Bean
WebClient inventoryWebClient(WebClient.Builder builder) {
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000)
.responseTimeout(Duration.ofMillis(1500))
.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(1500, TimeUnit.MILLISECONDS))
.addHandlerLast(new WriteTimeoutHandler(1500, TimeUnit.MILLISECONDS))
);
return builder
.baseUrl("http://inventory-service")
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
}Notas prácticas: (1) Ajusta timeouts según SLOs; (2) evita timeouts “infinitos”; (3) diferencia entre timeout de conexión y de respuesta.
Paso 2: realizar una llamada con timeout por request y manejo de errores
@Service
public class InventoryClient {
private final WebClient webClient;
public InventoryClient(WebClient inventoryWebClient) {
this.webClient = inventoryWebClient;
}
public Mono<InventoryResponse> getAvailability(String sku, String traceId) {
return webClient.get()
.uri(uriBuilder -> uriBuilder.path("/api/inventory/{sku}").build(sku))
.header("X-Trace-Id", traceId)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, resp -> resp.createException())
.onStatus(HttpStatusCode::is5xxServerError, resp -> resp.createException())
.bodyToMono(InventoryResponse.class)
.timeout(Duration.ofMillis(1200));
}
}La clave es que el timeout sea menor que el presupuesto de latencia total de tu endpoint. Si tu API tiene 2s de SLO, no gastes 1.9s en una dependencia.
Reintentos: cuándo sí y cuándo no
Los reintentos pueden mejorar disponibilidad percibida ante fallos transitorios (picos, resets, timeouts puntuales), pero también pueden empeorar una degradación (más carga) si se aplican indiscriminadamente.
Reglas de oro
- Reintenta solo operaciones idempotentes (GET, PUT idempotente) o cuando tengas una clave de idempotencia.
- Usa backoff (espera creciente) y jitter (aleatoriedad) para evitar thundering herd.
- Limita intentos (p. ej., 2 o 3) y respeta el timeout global.
- No reintentes 4xx (salvo 429 con política específica).
Circuit breaker: cortar para sobrevivir
El circuit breaker evita que un servicio siga golpeando a una dependencia que está fallando o lenta. Cuando el circuito está abierto, las llamadas fallan rápido y se activa un fallback (si existe). Esto protege hilos, conexiones y colas internas, y reduce la cascada de fallos.
Guía práctica: Resilience4j con WebClient
Paso 1: dependencias (referencia)
Incluye los starters de Resilience4j para Spring Boot y, si aplica, Actuator para métricas. (Los nombres exactos dependen de tu versión de Spring Boot.)
Paso 2: configuración de circuit breaker y retry
resilience4j.circuitbreaker.instances.inventory.registerHealthIndicator=true
resilience4j.circuitbreaker.instances.inventory.slidingWindowType=COUNT_BASED
resilience4j.circuitbreaker.instances.inventory.slidingWindowSize=20
resilience4j.circuitbreaker.instances.inventory.failureRateThreshold=50
resilience4j.circuitbreaker.instances.inventory.waitDurationInOpenState=10s
resilience4j.circuitbreaker.instances.inventory.permittedNumberOfCallsInHalfOpenState=5
resilience4j.retry.instances.inventory.maxAttempts=3
resilience4j.retry.instances.inventory.waitDuration=200ms
resilience4j.retry.instances.inventory.enableExponentialBackoff=true
resilience4j.retry.instances.inventory.exponentialBackoffMultiplier=2Interpretación: si en una ventana de 20 llamadas fallan ≥50%, el circuito abre 10s. En half-open prueba 5 llamadas para decidir si cierra.
Paso 3: aplicar circuit breaker + retry + fallback
Una forma práctica es decorar el Mono con operadores de Resilience4j. Ejemplo conceptual:
@Service
public class InventoryClient {
private final WebClient webClient;
private final CircuitBreaker cb;
private final Retry retry;
public InventoryClient(WebClient inventoryWebClient, CircuitBreakerRegistry cbRegistry, RetryRegistry retryRegistry) {
this.webClient = inventoryWebClient;
this.cb = cbRegistry.circuitBreaker("inventory");
this.retry = retryRegistry.retry("inventory");
}
public Mono<InventoryResponse> getAvailability(String sku, String traceId) {
Mono<InventoryResponse> call = webClient.get()
.uri("/api/inventory/{sku}", sku)
.header("X-Trace-Id", traceId)
.retrieve()
.bodyToMono(InventoryResponse.class)
.timeout(Duration.ofMillis(1200));
return call
.transformDeferred(CircuitBreakerOperator.of(cb))
.transformDeferred(RetryOperator.of(retry))
.onErrorResume(ex -> fallbackAvailability(sku, ex));
}
private Mono<InventoryResponse> fallbackAvailability(String sku, Throwable ex) {
// Degradación controlada: responder “desconocido” o usar caché
return Mono.just(InventoryResponse.unknown(sku));
}
}Orden recomendado: primero circuit breaker (fail-fast si está abierto), luego retry para transitorios cuando el circuito está cerrado. Ajusta según tu caso.
Idempotencia: evitar efectos duplicados
En microservicios, los duplicados ocurren por reintentos del cliente, reentrega de mensajes, timeouts ambiguos (el servidor procesó pero el cliente no recibió respuesta) y fallos parciales. La idempotencia asegura que repetir la misma operación no cause efectos adicionales.
Idempotencia en REST
- GET debe ser idempotente por definición.
- PUT suele ser idempotente si reemplaza el recurso completo o aplica un estado final determinista.
- POST normalmente no es idempotente; para hacerlo idempotente, usa una Idempotency-Key.
Patrón: Idempotency-Key para POST
Flujo típico: el cliente envía Idempotency-Key (UUID) y el servidor guarda el resultado asociado a esa clave durante un TTL. Si llega el mismo key, devuelve el mismo resultado sin duplicar efectos.
POST /api/payments
Idempotency-Key: 2f6c2b7a-1a6b-4b1f-9c2e-9d6c2c6f1a11
X-Trace-Id: 8c1d...
{ "orderId": "123", "amount": 49.90 }Implementación conceptual: tabla/almacén idempotency_keys con (key, endpoint, requestHash, response, status, createdAt, ttl). Si el requestHash difiere para la misma key, responde 409 para evitar ambigüedad.
Idempotencia en eventos
En mensajería, asume al menos una vez: un evento puede llegar duplicado. Solución: consumer idempotent con deduplicación por eventId o por clave de negocio.
- Guarda
eventIdprocesados (con TTL si el volumen es alto). - Haz que las escrituras sean idempotentes (upsert, compare-and-set, versionado).
Correlación y propagación de headers (traceId)
Para depurar flujos distribuidos necesitas un identificador de correlación. Un enfoque simple es propagar X-Trace-Id (o usar estándares como W3C traceparent). La idea: si una petición entra con un traceId, se reutiliza; si no, se genera y se propaga a todas las dependencias y eventos.
Paso a paso: generar y propagar X-Trace-Id en Spring
Paso 1: filtro de entrada (Servlet) para asegurar traceId
@Component
public class TraceIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String traceId = request.getHeader("X-Trace-Id");
if (traceId == null || traceId.isBlank()) {
traceId = UUID.randomUUID().toString();
}
MDC.put("traceId", traceId);
response.setHeader("X-Trace-Id", traceId);
try {
filterChain.doFilter(request, response);
} finally {
MDC.remove("traceId");
}
}
}Esto permite que tus logs incluyan traceId (configurando el patrón de logging para imprimir %X{traceId}).
Paso 2: propagar header en WebClient automáticamente
@Configuration
public class WebClientTraceConfig {
@Bean
WebClient.Builder webClientBuilderWithTrace() {
return WebClient.builder()
.filter((request, next) -> {
String traceId = MDC.get("traceId");
ClientRequest newReq = ClientRequest.from(request)
.header("X-Trace-Id", traceId != null ? traceId : "")
.build();
return next.exchange(newReq);
});
}
}Si usas programación reactiva end-to-end, evita depender de MDC (thread-local) y usa el Context de Reactor para transportar el traceId; el objetivo es el mismo: propagar el identificador.
Qué headers suele ser útil propagar
- X-Trace-Id o traceparent: correlación.
- Authorization: solo si el servicio downstream debe actuar en nombre del usuario; considera tokens “on-behalf-of” o tokens internos.
- X-Request-Id: id único por request (puede coexistir con traceId).
- Accept-Language o preferencias: si afectan la respuesta.
Evita propagar headers sensibles sin necesidad (cookies, datos personales) y define una lista explícita de allowlist.
Evitar cascadas de fallos: patrones de diseño
1) Timeouts estrictos y presupuestos de latencia
Divide el tiempo total permitido entre dependencias. Ejemplo: si tu endpoint debe responder en 800ms, y llamas a 2 servicios, podrías asignar 200ms a cada uno + 200ms de procesamiento local + margen. Si una dependencia no responde en su presupuesto, falla rápido y degrada.
2) Bulkhead (compartimentos estancos)
Evita que una dependencia lenta consuma todos los recursos. Se puede aplicar con límites de concurrencia por cliente remoto (pool/semáforo) o colas separadas. Conceptualmente: “aunque Inventory esté lento, no debe tumbar el resto del servicio”.
3) Cache y stale-while-revalidate
Para lecturas, una caché local o distribuida puede servir datos recientes si el remoto falla. Patrón: devolver dato stale (con marca de antigüedad) y refrescar en segundo plano cuando el circuito se recupere.
4) Degradación controlada: fallbacks y respuestas parciales
Un fallback no es “inventar datos”; es definir una respuesta útil y segura cuando falta una dependencia. Dos estrategias comunes:
- Fallback funcional: usar una fuente alternativa (caché, réplica, snapshot).
- Fallback informativo: responder “desconocido” o “temporalmente no disponible” para una parte del payload.
Ejemplo: respuesta parcial en un agregador
Supón un endpoint /api/orders/{id}/view que agrega: pedido + estado de pago + disponibilidad. Si Inventory falla, aún puedes devolver el pedido y el pago, marcando disponibilidad como desconocida.
public Mono<OrderView> getOrderView(String orderId) {
Mono<Order> orderMono = orderService.get(orderId);
Mono<PaymentStatus> paymentMono = paymentClient.getStatus(orderId)
.onErrorReturn(PaymentStatus.unknown());
Mono<InventoryResponse> invMono = inventoryClient.getAvailabilityForOrder(orderId)
.onErrorReturn(InventoryResponse.unknownForOrder(orderId));
return Mono.zip(orderMono, paymentMono, invMono)
.map(tuple -> new OrderView(tuple.getT1(), tuple.getT2(), tuple.getT3()));
}Esto reduce el impacto de un fallo parcial y mejora la experiencia. Acompáñalo con un campo de “calidad” o “fuente” para que el frontend decida cómo mostrarlo.
5) Evitar “chatty calls” y N+1 entre servicios
Muchas llamadas pequeñas aumentan latencia y puntos de fallo. Alternativas: endpoints de “batch”, composición por eventos (materializar vistas), o ajustar el contrato para traer lo necesario en una sola llamada.
Comunicación asíncrona con eventos: diseño resiliente
Eventos de dominio y contratos
Un evento debe ser un hecho pasado, inmutable y con identidad. Incluye metadatos para trazabilidad:
- eventId (UUID)
- eventType y version
- occurredAt
- traceId y/o correlationId
- payload con datos mínimos necesarios
{
"eventId": "b2b0f9d1-5a2a-4b0f-8d1f-6c0a1b2c3d4e",
"eventType": "OrderCreated",
"version": 1,
"occurredAt": "2026-02-03T10:15:30Z",
"traceId": "8c1d...",
"payload": {
"orderId": "123",
"customerId": "C-9",
"total": 49.90
}
}Outbox (concepto) para evitar pérdida de eventos
Problema: si guardas datos y publicas un evento, puede fallar una de las dos cosas y quedar inconsistente. El patrón Outbox guarda el evento en una tabla/cola local junto con el cambio de estado, y un publicador lo envía de forma confiable. No necesitas repetir detalles de persistencia aquí; quédate con la idea: publicación confiable y reintentos controlados.
Consumidores resilientes: reintentos y DLQ
- Reintentos con backoff para errores transitorios.
- DLQ (dead-letter queue) para mensajes que fallan repetidamente, evitando bloquear la cola principal.
- Idempotencia por eventId para tolerar duplicados.
Checklist de implementación para resiliencia (aplicable a REST y eventos)
- Define timeouts por dependencia y un timeout global por request.
- Aplica retry solo donde sea seguro (idempotencia) y con backoff + jitter.
- Usa circuit breaker para fail-fast y proteger recursos.
- Implementa fallbacks y/o respuestas parciales para degradación controlada.
- Propaga traceId (y correlationId si aplica) en HTTP y eventos.
- Diseña consumidores de eventos como idempotentes y con DLQ.
- Evita llamadas excesivas entre servicios; prefiere batch o vistas materializadas cuando el caso lo justifique.