9 min read
Tenés un servicio que consulta la API (Interfaz de Programación de Aplicaciones) de un proveedor de pagos externo. Todo funciona perfecto durante semanas. Un día, ese proveedor empieza a tener problemas: algunas llamadas tardan 30 segundos en responder, otras simplemente no responden. En cuestión de minutos, los hilos de tu aplicación se saturan esperando respuestas que nunca llegan. Tu servicio — que técnicamente no tiene ningún bug — colapsa por culpa de algo que está completamente fuera de tu control.
Este es uno de los escenarios más comunes y más dolorosos en sistemas distribuidos. Y es exactamente el problema que resuelven dos patrones de resiliencia fundamentales: Retry y Circuit Breaker (Interruptor de Circuito).
En este post vamos a ver qué hace cada uno, cuándo usarlos juntos, y cómo implementarlos en Java desde cero con un ejemplo real.
¿Qué es el Patrón Retry?
El patrón Retry es simple en su concepto: si una operación falla, inténtala de nuevo. Pero bien implementado tiene matices importantes.
No todos los fallos merecen un retry. Hay dos tipos:
- Fallos transitorios: problemas temporales que pueden resolverse solos. Un timeout (tiempo de espera agotado) de red, una sobrecarga momentánea del servidor, un pico de tráfico. Estos sí son candidatos para retry.
- Fallos permanentes: el recurso no existe, las credenciales son inválidas, el request (solicitud) está mal formado. Reintentar aquí es desperdiciar recursos.
Además, un retry mal implementado puede empeorar las cosas. Si tu servicio falla y diez clientes hacen retry al mismo tiempo, acabás de multiplicar la carga sobre un servidor que ya está en problemas. Por eso el retry serio siempre incluye Exponential Backoff (espera exponencial entre intentos) y opcionalmente Jitter (variación aleatoria) para distribuir la carga.
| Intento | Espera base | Con Jitter |
|---|---|---|
| 1 | 1s | ~0.8s – 1.2s |
| 2 | 2s | ~1.6s – 2.4s |
| 3 | 4s | ~3.2s – 4.8s |
| 4 | 8s | ~6.4s – 9.6s |

¿Qué es el Patrón Circuit Breaker?
El Circuit Breaker (Interruptor de Circuito) toma su nombre de los interruptores eléctricos: cuando hay una sobrecarga, el circuito se abre para proteger el sistema. Luego, cuando las condiciones vuelven a la normalidad, se cierra de nuevo.
Tiene tres estados:
- CLOSED (Cerrado): todo funciona normal. Las llamadas pasan al servicio externo. Si los fallos superan un umbral definido, el circuito se abre.
- OPEN (Abierto): el circuito está abierto. Las llamadas no llegan al servicio externo — se bloquean de inmediato y retornan un error (o un fallback, es decir una respuesta alternativa predefinida). Esto protege al servicio externo de recibir más carga mientras está en problemas.
- HALF-OPEN (Semi-abierto): después de un tiempo de espera, el circuito permite pasar algunas llamadas de prueba. Si tienen éxito, el circuito vuelve a CLOSED. Si fallan, vuelve a OPEN.

Guía paso a paso
Paso 1 — Identificar qué operaciones necesitan resiliencia
No todo necesita Retry ni Circuit Breaker. Enfocate en llamadas a servicios externos: APIs de terceros, bases de datos remotas, servicios de mensajería. Las operaciones internas raramente las necesitan.
Paso 2 — Implementar Retry con Exponential Backoff
Definí el número máximo de intentos y el tiempo de espera base. Con cada intento fallido, duplicás la espera. Agregá jitter para evitar que todos los clientes reintenten al mismo tiempo.
Paso 3 — Envolver las llamadas con el Circuit Breaker
El Circuit Breaker envuelve la operación (incluyendo el Retry). Define cuántos fallos consecutivos abren el circuito y cuánto tiempo permanece abierto antes de pasar a HALF-OPEN.
Paso 4 — Definir un Fallback
Cuando el circuito está abierto, ¿qué retornás? Podría ser un valor por defecto, una respuesta cacheada, o un mensaje de error controlado. Lo importante es que el sistema siga respondiendo — aunque sea de forma degradada.
Paso 5 — Agregar observabilidad
Logeá cada cambio de estado del Circuit Breaker y cada intento de Retry. En producción, estos logs son oro para diagnosticar problemas con servicios externos.
El código
El ejemplo modela una llamada real a una API de pagos externa que falla intermitentemente. Implementamos Retry con Exponential Backoff y Circuit Breaker desde cero en Java, sin librerías externas.
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.time.Instant;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
// --- Simulación de la API externa de pagos ---
class PaymentGatewayClient {
private static final Random random = new Random();
public String processPayment(String orderId, double amount) throws Exception {
if (random.nextDouble() < 0.6) {
throw new Exception("Payment gateway timeout for order: " + orderId);
}
return "TXN-" + orderId + "-OK";
}
}
// --- Patrón Retry con Exponential Backoff ---
class RetryPolicy {
private final int maxAttempts;
private final long baseDelayMs;
public RetryPolicy(int maxAttempts, long baseDelayMs) {
this.maxAttempts = maxAttempts;
this.baseDelayMs = baseDelayMs;
}
public <T> T execute(SupplierWithException<T> operation) throws Exception {
int attempt = 0;
while (true) {
try {
attempt++;
System.out.println(" [Retry] Intento #" + attempt);
return operation.get();
} catch (Exception e) {
if (attempt >= maxAttempts) {
System.out.println(" [Retry] Máximo de intentos alcanzado (" + maxAttempts + "). Propagando error.");
throw e;
}
long delay = baseDelayMs * (1L << (attempt - 1));
long jitter = (long) (Math.random() * delay * 0.2);
long totalDelay = delay + jitter;
System.out.println(" [Retry] Fallo en intento #" + attempt + ". Esperando " + totalDelay + "ms.");
Thread.sleep(totalDelay);
}
}
}
}
@FunctionalInterface
interface SupplierWithException<T> {
T get() throws Exception;
}
// --- Estados del Circuit Breaker ---
enum CircuitState {
CLOSED, OPEN, HALF_OPEN
}
// --- Patrón Circuit Breaker ---
class CircuitBreaker {
private final String name;
private final int failureThreshold;
private final long openDurationMs;
private CircuitState state = CircuitState.CLOSED;
private final AtomicInteger failureCount = new AtomicInteger(0);
private Instant openedAt;
public CircuitBreaker(String name, int failureThreshold, long openDurationMs) {
this.name = name;
this.failureThreshold = failureThreshold;
this.openDurationMs = openDurationMs;
}
public <T> T execute(SupplierWithException<T> operation, SupplierWithException<T> fallback) throws Exception {
if (state == CircuitState.OPEN) {
if (Instant.now().isAfter(openedAt.plusMillis(openDurationMs))) {
transitionTo(CircuitState.HALF_OPEN);
} else {
System.out.println("[CircuitBreaker:" + name + "] Circuito ABIERTO — usando fallback.");
return fallback.get();
}
}
try {
T result = operation.get();
onSuccess();
return result;
} catch (Exception e) {
onFailure();
if (state == CircuitState.OPEN) {
System.out.println("[CircuitBreaker:" + name + "] Circuito recién ABIERTO — usando fallback.");
return fallback.get();
}
throw e;
}
}
private void onSuccess() {
failureCount.set(0);
if (state == CircuitState.HALF_OPEN) {
transitionTo(CircuitState.CLOSED);
}
}
private void onFailure() {
int failures = failureCount.incrementAndGet();
System.out.println("[CircuitBreaker:" + name + "] Fallo registrado. Total: " + failures + "/" + failureThreshold);
if (failures >= failureThreshold && state != CircuitState.OPEN) {
transitionTo(CircuitState.OPEN);
}
}
private void transitionTo(CircuitState newState) {
System.out.println("[CircuitBreaker:" + name + "] Estado: " + state + " → " + newState);
this.state = newState;
if (newState == CircuitState.OPEN) {
this.openedAt = Instant.now();
}
}
}
// --- Servicio de pagos con ambos patrones combinados ---
class PaymentService {
private final PaymentGatewayClient gatewayClient;
private final RetryPolicy retryPolicy;
private final CircuitBreaker circuitBreaker;
public PaymentService(PaymentGatewayClient gatewayClient, RetryPolicy retryPolicy, CircuitBreaker circuitBreaker) {
this.gatewayClient = gatewayClient;
this.retryPolicy = retryPolicy;
this.circuitBreaker = circuitBreaker;
}
public String processPayment(String orderId, double amount) throws Exception {
return circuitBreaker.execute(
() -> retryPolicy.execute(
() -> gatewayClient.processPayment(orderId, amount)
),
() -> {
System.out.println(" [Fallback] Pago encolado para procesamiento posterior.");
return "PENDING-" + orderId;
}
);
}
}
// --- Main ---
public class ResiliencePatternDemo {
public static void main(String[] args) throws Exception {
PaymentGatewayClient gatewayClient = new PaymentGatewayClient();
RetryPolicy retryPolicy = new RetryPolicy(3, 500);
CircuitBreaker circuitBreaker = new CircuitBreaker("PaymentGateway", 3, 5000);
PaymentService paymentService = new PaymentService(gatewayClient, retryPolicy, circuitBreaker);
String[] orders = {"ORD-001", "ORD-002", "ORD-003", "ORD-004", "ORD-005"};
for (String orderId : orders) {
System.out.println("\n=== Procesando " + orderId + " ===");
try {
String result = paymentService.processPayment(orderId, 99.99);
System.out.println("Resultado: " + result);
} catch (Exception e) {
System.out.println("Error final para " + orderId + ": " + e.getMessage());
}
}
}
}
Output esperado (varía por aleatoriedad):
=== Procesando ORD-001 ===
[Retry] Intento #1
[Retry] Fallo en intento #1. Esperando 512ms.
[Retry] Intento #2
[CircuitBreaker:PaymentGateway] Fallo registrado. Total: 1/3
...
[CircuitBreaker:PaymentGateway] Estado: CLOSED → OPEN
[CircuitBreaker:PaymentGateway] Circuito recién ABIERTO — usando fallback.
[Fallback] Pago encolado para procesamiento posterior.
Resultado: PENDING-ORD-001
=== Procesando ORD-002 ===
[CircuitBreaker:PaymentGateway] Circuito ABIERTO — usando fallback.
[Fallback] Pago encolado para procesamiento posterior.
Resultado: PENDING-ORD-002
Para tener en cuenta en producción
Estos patrones parecen simples, pero hay decisiones de configuración que pueden marcar la diferencia entre proteger tu sistema o dejarlo igual de expuesto:
- Los umbrales del Circuit Breaker son críticos: si abrís el circuito con muy pocos fallos, bloqueás tráfico válido. Si lo abrís con demasiados, no protegés nada. Calibralo con datos reales de tu SLA (Service Level Agreement — Acuerdo de Nivel de Servicio) con el proveedor externo.
- No todos los errores deben contar: un error 400 (Bad Request — Solicitud Incorrecta) es un error tuyo, no del servicio externo. El Circuit Breaker debería ignorar ese tipo de errores y solo contar timeouts y errores 5xx (errores del servidor).
- El fallback es parte del diseño, no un parche: definí qué pasa cuando el circuito está abierto desde el inicio del diseño. ¿Encolás para después? ¿Retornás una respuesta cacheada? ¿Mostrás un error amigable al usuario?
- Resilience4j para producción: el código que vimos es una implementación didáctica. En proyectos reales, usá Resilience4j, la librería estándar de resiliencia para Java. Ofrece Circuit Breaker, Retry, Rate Limiter (Limitador de Tasa de Solicitudes) y Bulkhead (Aislamiento de Recursos) con configuración declarativa y métricas listas para conectar a tu sistema de monitoreo.
Conclusión
Retry y Circuit Breaker son dos patrones que se complementan perfectamente. El Retry se encarga de los fallos transitorios — esos pequeños tropiezos que se resuelven solos con un poco de paciencia. El Circuit Breaker se encarga de los fallos sostenidos — cuando un servicio externo está genuinamente caído y lo más inteligente es dejar de llamarlo por un rato.
Usados juntos, tu servicio sigue respondiendo aunque el mundo exterior esté en llamas. Y eso, en producción, vale todo.
En el próximo post vamos a explorar SOLID en la práctica: cómo aplicar los cinco principios (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation y Dependency Inversion — Responsabilidad Única, Abierto/Cerrado, Sustitución de Liskov, Segregación de Interfaces e Inversión de Dependencias) con ejemplos antes y después de refactorización real.
Recordá suscribirte aquí para recibir los próximos posts directamente en tu correo.
Referencias:
Release It! Design and Deploy Production-Ready Software — Michael T. Nygard
Building Microservices — Sam Newman
Resilience4j Documentation — https://resilience4j.readme.io/