Comunicación entre microservicios y resiliencia

Capítulo 5

Tiempo estimado de lectura: 11 minutos

+ Ejercicio

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

CriterioREST (síncrono)Eventos (asíncrono)
Necesidad de respuesta inmediataAltaBaja/Media
Acoplamiento temporal (si el otro cae)AltoBajo
ConsistenciaMás inmediataEventual
ComplejidadMenorMayor (reintentos, duplicados, orden)
ObservabilidadMás directaRequiere 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.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

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=2

Interpretació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 eventId procesados (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.

Ahora responde el ejercicio sobre el contenido:

En una arquitectura de microservicios, ¿qué combinación describe mejor una estrategia para evitar cascadas de fallos al hacer llamadas REST a una dependencia inestable?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

Los timeouts evitan bloquear recursos, el circuit breaker corta rápido cuando la dependencia está degradada y los reintentos deben usarse con límites y solo si es seguro (idempotencia), preferiblemente con backoff y jitter para no aumentar la carga.

Siguiente capítulo

Seguridad de microservicios con Spring Boot y OAuth2

Arrow Right Icon
Portada de libro electrónico gratuitaMicroservicios con Spring Boot desde Cero
42%

Microservicios con Spring Boot desde Cero

Nuevo curso

12 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.