el anti-patrón: un único intento

retry con backoff exponencial
Reintentar de inmediato satura al servicio. Con backoff exponencial esperamos 1s, 2s, 4s, 8s… hasta un máximo de intentos. Si finalmente falla, degradamos la respuesta o vamos a una cola para reprocesar.

// build.gradle (o pom.xml): agrega resilience4j-spring-boot y spring-boot-starter-aop
// implementation "io.github.resilience4j:resilience4j-spring-boot3"
// implementation "org.springframework.boot:spring-boot-starter-aop"
@Service
public class ApiClient {
private final RestClient rest; // usa RestTemplate/WebClient/RestClient
public ApiClient(RestClient.Builder builder) {
this.rest = builder.baseUrl("https://api.ejemplo.com").build();
}
@Retry(name = "apiRetry", fallbackMethod = "fallback")
public String getDato() {
return rest.get().retrieve().body(String.class);
}
// fallback si agotó reintentos
private String fallback(Throwable ex) {
return "{ \"status\": \"degradado\", \"msg\": \"usando cache\" }";
}
}
// application.yml (resilience4j-retry)
resilience4j:
retry:
instances:
apiRetry:
max-attempts: 5
wait-duration: 1s
enable-exponential-backoff: true
exponential-backoff-multiplier: 2.0
retry-exceptions:
- org.springframework.web.client.HttpServerErrorException
- java.io.IOException
Habilita AOP para que las anotaciones funcionen.
# pip install tenacity requests
import requests
from tenacity import retry, wait_exponential, stop_after_attempt, retry_if_exception_type
@retry(
retry=retry_if_exception_type((requests.exceptions.RequestException,)),
wait=wait_exponential(multiplier=1, min=1, max=16),
stop=stop_after_attempt(5),
reraise=True
)
def fetch():
r = requests.get("https://api.ejemplo.com/dato", timeout=3)
r.raise_for_status()
return r.json()
if __name__ == "__main__":
try:
print(fetch())
except Exception as e:
print({"status":"degradado","msg":"usar cache","error":str(e)})
Backoff exponencial 1→16s, 5 intentos.
circuit breaker (no dejes colgado al cliente)
Si un downstream falla mucho, **abrimos** el circuito y devolvemos **fallback** inmediato, evitando timeouts y tormentas de reintentos. Luego pasamos a **half-open** para probar un request y reabrir si mejora.


@Service
public class PagoService {
private final RestClient rest;
public PagoService(RestClient.Builder builder) {
this.rest = builder.baseUrl("https://pagos.ejemplo.com").build();
}
@CircuitBreaker(name = "pagoCB", fallbackMethod = "fallbackPago")
@Retry(name = "pagoRetry") // opcional: CB + Retry
public String cobrar() {
return rest.post().retrieve().body(String.class);
}
String fallbackPago(Throwable ex) {
// no bloqueamos al cliente; almacenamos en cola para reprocesar
return "{ \"status\": \"pendiente\", \"msg\": \"se agendó el cobro\" }";
}
}
// application.yml
resilience4j:
circuitbreaker:
instances:
pagoCB:
failure-rate-threshold: 50 # % fallos para abrir
sliding-window-size: 20
wait-duration-in-open-state: 10s # tiempo abierto antes de half-open
permitted-number-of-calls-in-half-open-state: 1
Combina CB + Retry para mayor robustez.
# pip install pybreaker requests
import requests
import pybreaker
breaker = pybreaker.CircuitBreaker(
fail_max=10, # fallos para abrir
reset_timeout=10 # permanecer abierto (s)
)
@breaker
def pagar():
r = requests.post("https://pagos.ejemplo.com/cobrar", timeout=3)
r.raise_for_status()
return r.json()
def pagar_con_fallback():
try:
return pagar()
except pybreaker.CircuitBreakerError:
# abierto: no intentamos; respondemos rápido
return {"status":"pendiente","msg":"se agendó el cobro"}
except Exception:
return {"status":"pendiente","msg":"reintentar más tarde"}
Fallback inmediato cuando el circuito está abierto.
retry pattern en kafka (topics retry + error)
Para eventos, reintentar dentro del mismo consumer puede bloquear la partición. Un patrón común es publicar en **topic-retry** con backoff (o varios topics con TTL/delay) y, si se agotan los intentos, enviar a **DLT** (dead-letter topic) para remediación.

// build.gradle: implementation "org.springframework.kafka:spring-kafka"
@Configuration
@EnableKafka
public class KafkaConfig {
@Bean
public DefaultKafkaConsumerFactory<String, String> consumerFactory() {
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ConsumerConfig.GROUP_ID_CONFIG, "app-1");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
return new DefaultKafkaConsumerFactory<>(props);
}
// Retry con backoff + DLQ (desde Spring Kafka 2.8+)
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
var factory = new ConcurrentKafkaListenerContainerFactory<String, String>();
factory.setConsumerFactory(consumerFactory());
// Backoff exponencial (1s,2s,4s,8s, … máx 5 intentos)
var backOff = new ExponentialBackOffWithMaxRetries(5);
backOff.setInitialInterval(1000);
backOff.setMultiplier(2.0);
backOff.setMaxInterval(8000);
var recoverer = new DeadLetterPublishingRecoverer(kafkaTemplate()); // envía a topic-ERROR
var errorHandler = new DefaultErrorHandler(recoverer, backOff);
factory.setCommonErrorHandler(errorHandler);
return factory;
}
@KafkaListener(topics = "topic-origen")
public void procesar(String payload) {
// si lanza excepción, aplicará backoff y al final publicará en topic-origen.DLT
procesarEnBD(payload);
}
@Bean
KafkaTemplate<String,String> kafkaTemplate() { /* ... */ }
}
Backoff exponencial y DeadLetterPublishingRecoverer.
# pip install confluent-kafka
from confluent_kafka import Consumer, Producer
import time, json
c = Consumer({"bootstrap.servers":"localhost:9092","group.id":"app-1","auto.offset.reset":"earliest"})
p = Producer({"bootstrap.servers":"localhost:9092"})
c.subscribe(["topic-origen"])
def procesar(msg):
data = json.loads(msg.value())
# lanzar excepción si algo sale mal para simular fallo
if data.get("fail"):
raise RuntimeError("fallo")
# … guardar en BD …
while True:
msg = c.poll(1.0)
if not msg: continue
try:
procesar(msg)
except Exception:
# publica en retry con un header para 'intento'
intento = int(dict(msg.headers() or {}).get("retry", 0)) + 1
delay = min(2 ** intento, 16) # 1,2,4,8,16…
time.sleep(delay)
p.produce("topic-retry", msg.value(), headers=[("retry", str(intento))])
finally:
c.commit(asynchronous=False)
Ejemplo conceptual; en prod usa múltiples topics retry (1m, 5m, 15m…).
reglas rápidas
- Idempotencia: diseña operaciones seguras ante reintentos.
- Timeouts: siempre más cortos que los del LB/ingress.
- Jitter: añade aleatoriedad al backoff para evitar sincronía.
- Observabilidad: métricas de tasa de fallos, estado del CB, tamaño de DLT.
- UX: comunica “en proceso” cuando devuelves fallback.