karawaci.kode

← Semua snippet

Java Lanjut Performance

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 @Retry eksekusi 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