Spring + Resilience4j circuit breaker
Circuit breaker Resilience4j melindungi service dari cascading failure — kalau downstream lambat, fail fast bukan thread pool habis.
Dipublikasikan 9 Juli 2026
Service A call B yang lagi sakit — request menumpuk, thread pool A habis, A ikut down. Cascading failure klasik. Circuit breaker putus call ke B saat B di atas threshold error, kasih waktu B recover. Snippet ini config Resilience4j di Spring Boot untuk HTTP call ke payment gateway eksternal.
Kode
# application.yml
resilience4j:
circuitbreaker:
instances:
paymentGateway:
# Sliding window — observasi
sliding-window-type: COUNT_BASED
sliding-window-size: 100 # observasi 100 call terakhir
minimum-number-of-calls: 20 # minimum sebelum decide open
# Threshold
failure-rate-threshold: 50 # > 50% error → OPEN
slow-call-duration-threshold: 2s # call > 2s = slow
slow-call-rate-threshold: 80 # > 80% slow → OPEN
# State transition
wait-duration-in-open-state: 30s
permitted-number-of-calls-in-half-open-state: 5
# Auto transition open → half-open setelah wait
automatic-transition-from-open-to-half-open-enabled: true
# Record specific exceptions
record-exceptions:
- java.io.IOException
- org.springframework.web.client.HttpServerErrorException
- java.util.concurrent.TimeoutException
ignore-exceptions:
- id.kodekarawaci.exception.UserInputException
inventoryApi:
sliding-window-size: 50
failure-rate-threshold: 40
wait-duration-in-open-state: 15s
slow-call-duration-threshold: 1s
retry:
instances:
paymentGateway:
max-attempts: 3
wait-duration: 500ms
enable-exponential-backoff: true
exponential-backoff-multiplier: 2
retry-exceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
timelimiter:
instances:
paymentGateway:
timeout-duration: 5s
cancel-running-future: true
# Expose metric ke Actuator / Prometheus
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus,circuitbreakers
health:
circuitbreakers:
enabled: true
metrics:
distribution:
percentiles:
resilience4j.circuitbreaker.calls: 0.5, 0.95, 0.99
// PaymentService.java
package id.kodekarawaci.service;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;
import io.github.resilience4j.timelimiter.annotation.TimeLimiter;
import java.util.concurrent.CompletableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
@Service
public class PaymentService {
private static final Logger LOG = LoggerFactory.getLogger(PaymentService.class);
private final RestClient restClient;
public PaymentService(RestClient.Builder builder) {
this.restClient = builder
.baseUrl("https://payment-gateway.tangerang.id")
.build();
}
/**
* Synchronous call dengan circuit breaker + retry + fallback.
* Order eksekusi: TimeLimiter → CircuitBreaker → Retry → method
*/
@CircuitBreaker(name = "paymentGateway", fallbackMethod = "chargeFallback")
@Retry(name = "paymentGateway")
public PaymentResult charge(ChargeRequest request) {
LOG.info("Charging amount={} via gateway", request.amount());
ResponseEntity<PaymentResult> response = restClient.post()
.uri("/v1/charge")
.body(request)
.retrieve()
.toEntity(PaymentResult.class);
return response.getBody();
}
/**
* Fallback dipanggil saat:
* - Circuit breaker OPEN
* - Retry exhausted
* - Exception yang di-record
*/
public PaymentResult chargeFallback(ChargeRequest request, Throwable t) {
LOG.warn("Payment fallback dipanggil — request={}, error={}",
request, t.getMessage());
// Strategy: queue ke retry queue untuk diproses async nanti
retryQueue.enqueue(request);
return PaymentResult.pending(request.idempotencyKey(),
"Payment sedang diproses, akan dikonfirmasi maks 5 menit");
}
/**
* Async variant dengan TimeLimiter — kalau lebih dari 5 detik, timeout.
*/
@CircuitBreaker(name = "paymentGateway", fallbackMethod = "chargeAsyncFallback")
@TimeLimiter(name = "paymentGateway")
public CompletableFuture<PaymentResult> chargeAsync(ChargeRequest request) {
return CompletableFuture.supplyAsync(() -> {
return restClient.post()
.uri("/v1/charge")
.body(request)
.retrieve()
.body(PaymentResult.class);
});
}
public CompletableFuture<PaymentResult> chargeAsyncFallback(
ChargeRequest request, Throwable t
) {
return CompletableFuture.completedFuture(
PaymentResult.pending(request.idempotencyKey(),
"Timeout, retry async: " + t.getClass().getSimpleName())
);
}
// Dummy retry queue
private final RetryQueue retryQueue = new RetryQueue();
static class RetryQueue {
void enqueue(ChargeRequest r) { /* push ke Redis / RabbitMQ */ }
}
}
record ChargeRequest(String idempotencyKey, Long userId, Long amount, String method) {}
record PaymentResult(String status, String transactionId, String message) {
public static PaymentResult pending(String key, String msg) {
return new PaymentResult("PENDING", key, msg);
}
}
Pemakaian
// Controller pakai PaymentService — circuit breaker transparent ke caller
@RestController
@RequestMapping("/api/checkout")
public class CheckoutController {
private final PaymentService paymentService;
private final OrderService orderService;
public CheckoutController(PaymentService p, OrderService o) {
this.paymentService = p;
this.orderService = o;
}
@PostMapping
public ResponseEntity<?> checkout(@RequestBody CheckoutRequest req) {
// Buat order dulu (idempotent dengan key)
var order = orderService.createOrder(req);
// Charge — kalau gateway down, fallback return PENDING
var paymentResult = paymentService.charge(new ChargeRequest(
order.idempotencyKey(),
req.userId(),
order.totalAmount(),
req.paymentMethod()
));
return ResponseEntity.ok(Map.of(
"order_id", order.id(),
"payment_status", paymentResult.status(),
"message", paymentResult.message()
));
}
}
# Trigger overload buat lihat circuit breaker open
for i in $(seq 1 200); do
curl -s -X POST http://localhost:8080/api/checkout \
-H "Content-Type: application/json" \
-d "{\"userId\":$i,\"paymentMethod\":\"qris\"}" &
done
# Cek status circuit breaker
curl http://localhost:8080/actuator/circuitbreakers
# {
# "paymentGateway": {
# "state": "OPEN",
# "failureRate": "68.5%",
# "bufferedCalls": 100,
# "failedCalls": 68,
# ...
# }
# }
# Setelah 30 detik (wait-duration-in-open-state):
# state HALF_OPEN — coba 5 call, kalau OK → CLOSED, kalau fail → OPEN lagi
# Query Prometheus
# Failure rate per circuit
resilience4j_circuitbreaker_failure_rate{name="paymentGateway"}
# State (0=closed, 1=open, 2=half-open)
resilience4j_circuitbreaker_state{name="paymentGateway"}
# Alert kalau OPEN > 5 menit
resilience4j_circuitbreaker_state{name="paymentGateway"} == 1
Kapan dipakai
- Call ke service eksternal yang flaky (payment gateway, SMS provider).
- Microservice yang depend ke service lain — protect dari cascading failure.
- Aggregator yang fan-out ke banyak backend.
- Integrasi dengan SaaS rate-limited.
Catatan
- Order matters —
@TimeLimiter @CircuitBreaker @Retryeksekusi dari luar ke dalam. TimeLimiter wrapping ke CircuitBreaker wrapping ke Retry wrapping ke method. Salah urutan = behavior beda. - Sliding window count vs time — count fixed jumlah call, time fixed window seconds. Count lebih predictable untuk low-traffic service.
- Minimum number of calls — sebelum minimum tercapai, circuit tidak akan OPEN walau 100% error. Set 20-50 untuk avoid false open di startup.
- Slow call rate — pattern penting: service yang lambat sama merusaknya dengan service yang error. Track latency, bukan cuma error rate.
- Half-open state — coba beberapa call buat test. Kalau sukses, CLOSED. Kalau fail, OPEN lagi. Tanpa half-open, circuit stuck.
- Idempotency wajib di downstream call — circuit breaker bisa retry. Tanpa idempotency, charge dobel.
- Fallback bukan silent — log + metric. Tahu kapan fallback aktif penting untuk diagnosis.
Jangan dekorasi method yang lokal (DB query yang biasanya cepat). Circuit breaker punya overhead kecil — gunakan untuk external network call atau slow service. Untuk DB, pakai timeout di driver saja.
# tags
resilience4jcircuit-breakerspringfault-tolerancejava
Ditulis oleh Asti Larasati · 9 Juli 2026