Tampilan
Kenapa penting
Di monolit, pemanggilan method hampir tidak pernah gagal. Di microservices, setiap panggilan melewati jaringan yang bisa lambat, putus, atau mengembalikan error — dan satu service yang lambat bisa menyeret seluruh sistem. Checkout mati bukan karena checkout rusak, tetapi karena service ongkir yang jarang dipakai menahan semua thread. Insiden production paling umum di sistem terdistribusi bukan bug logika, melainkan kegagalan yang merambat. Engineer backend diharapkan bisa merancang timeout, circuit breaker, bulkhead, dan retry yang membuat kegagalan tetap kecil, terisolasi, dan bisa dipulihkan.
Konsep inti
Cascading failure
Bayangkan rantai gateway → checkout → inventory → database. Database melambat dari 20 ms menjadi 5 detik. Thread inventory menunggu, pool-nya habis. checkout yang memanggil inventory kini ikut menunggu, pool-nya habis. Gateway melihat checkout tidak merespons. Dalam beberapa menit seluruh sistem tampak mati, padahal akar masalahnya satu query lambat.
Kegagalan merambat karena dua hal: menunggu terlalu lama dan tidak ada batas berapa banyak sumber daya yang boleh dihabiskan untuk satu dependency. Semua pola di bab ini menyerang salah satu dari dua hal itu.
Timeout budget berantai
Setiap panggilan jaringan wajib punya timeout. Tapi timeout per panggilan saja tidak cukup: bila gateway memberi 1 detik untuk seluruh permintaan, tidak ada gunanya checkout memberi inventory timeout 3 detik. Hasilnya datang setelah client sudah menyerah — kerja sia-sia.
Konsep deadline menyelesaikan ini: permintaan membawa batas waktu absolut, dan setiap langkah memakai min(sisa budget, batas per dependency).
java
import java.time.Duration;
import java.time.Instant;
public class TimeoutBudget {
record Deadline(Instant expiresAt) {
static Deadline after(Duration budget, Instant now) {
return new Deadline(now.plus(budget));
}
Duration remaining(Instant now) {
Duration left = Duration.between(now, expiresAt);
return left.isNegative() ? Duration.ZERO : left;
}
// Timeout panggilan berikutnya = yang lebih kecil antara sisa budget dan batas per dependency
Duration timeoutFor(Duration perCallLimit, Instant now) {
Duration left = remaining(now);
return left.compareTo(perCallLimit) < 0 ? left : perCallLimit;
}
}
public static void main(String[] args) {
Instant start = Instant.parse("2026-10-23T07:00:00Z");
Deadline deadline = Deadline.after(Duration.ofMillis(1000), start);
Instant afterInventory = start.plusMillis(350);
System.out.println("timeout payment : "
+ deadline.timeoutFor(Duration.ofMillis(800), afterInventory).toMillis() + " ms");
Instant afterPayment = start.plusMillis(950);
System.out.println("timeout shipping: "
+ deadline.timeoutFor(Duration.ofMillis(300), afterPayment).toMillis() + " ms");
Instant tooLate = start.plusMillis(1200);
Duration left = deadline.remaining(tooLate);
System.out.println(left.isZero()
? "budget habis -> gagal cepat, jangan panggil dependency"
: "masih ada " + left.toMillis() + " ms");
}
}text
timeout payment : 650 ms
timeout shipping: 50 ms
budget habis -> gagal cepat, jangan panggil dependencyAturan praktis: timeout di lapisan dalam harus lebih kecil daripada lapisan luar, dan bila budget sudah habis, jangan memanggil dependency sama sekali.
Circuit breaker per dependency
Circuit breaker memantau hasil panggilan ke satu dependency. Bila persentase gagal (atau lambat) melewati ambang, breaker membuka dan panggilan berikutnya langsung ditolak tanpa menyentuh jaringan.
| State | Perilaku |
|---|---|
CLOSED | Normal; hasil panggilan dicatat di sliding window |
OPEN | Semua panggilan ditolak seketika; dependency diberi waktu pulih |
HALF_OPEN | Beberapa panggilan percobaan diizinkan; sukses → CLOSED, gagal → OPEN |
Kata kuncinya per dependency: breaker untuk payment-gateway terpisah dari breaker untuk shipping-service. Satu breaker global akan mematikan semua panggilan hanya karena satu dependency bermasalah.
Spring Cloud Circuit Breaker + Resilience4j
Spring Cloud Circuit Breaker memberi abstraksi CircuitBreakerFactory; implementasi yang umum dipakai adalah Resilience4j lewat starter org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j.
java
// Potongan — memanggil inventory lewat circuit breaker bernama "inventory"
@Service
public class InventoryClient {
private final RestClient restClient;
private final CircuitBreakerFactory<?, ?> circuitBreakerFactory;
public InventoryClient(RestClient.Builder builder, CircuitBreakerFactory<?, ?> factory) {
this.restClient = builder.baseUrl("http://inventory-service").build();
this.circuitBreakerFactory = factory;
}
public StockStatus check(String sku) {
return circuitBreakerFactory.create("inventory").run(
() -> restClient.get().uri("/stock/{sku}", sku)
.retrieve().body(StockStatus.class),
throwable -> StockStatus.unknown(sku));
}
}Konfigurasi per instance ditulis di properti Resilience4j:
yaml
resilience4j:
circuitbreaker:
instances:
inventory:
sliding-window-type: COUNT_BASED
sliding-window-size: 20
minimum-number-of-calls: 10
failure-rate-threshold: 50
slow-call-duration-threshold: 500ms
slow-call-rate-threshold: 50
wait-duration-in-open-state: 10s
permitted-number-of-calls-in-half-open-state: 3
timelimiter:
instances:
inventory:
timeout-duration: 800msTime limiter bawaan
Integrasi Resilience4j di Spring Cloud Circuit Breaker membungkus panggilan dengan TimeLimiter, yang default-nya 1 detik. Panggilan yang wajar butuh 1,5 detik akan terus "gagal" bila kamu tidak menyetel timeout-duration secara eksplisit.
Bulkhead
Istilah ini diambil dari sekat kapal: bila satu kompartemen bocor, kapal tidak tenggelam. Bulkhead membatasi jumlah panggilan bersamaan ke satu dependency, sehingga service lambat hanya menghabiskan jatah kecil, bukan seluruh thread.
yaml
resilience4j:
bulkhead:
instances:
inventory:
max-concurrent-calls: 20
max-wait-duration: 0msDengan modul resilience4j-bulkhead di classpath, Spring Cloud Circuit Breaker ikut membungkus panggilan dengan bulkhead bernama sama. Bila 20 panggilan ke inventory sedang berjalan, panggilan ke-21 langsung ditolak (max-wait-duration: 0ms), sementara endpoint lain tetap punya thread.
Load shedding dan rate limit
Circuit breaker melindungi service dari dependency yang sakit; load shedding melindungi service dari dirinya sendiri yang kelebihan beban. Saat antrean penuh, lebih baik langsung menolak sebagian permintaan dengan 503 Service Unavailable daripada melayani semuanya dengan lambat hingga timeout. Rate limit membatasi jumlah permintaan per client atau per detik — biasanya di API gateway (bab sebelumnya) dan dijawab 429 Too Many Requests. Utamakan trafik penting: health check dan checkout didahulukan dibanding laporan atau rekomendasi.
Fallback aman vs gagal cepat
Fallback memberi jawaban pengganti saat dependency gagal. Fallback yang baik jujur dan tidak berbahaya:
| Situasi | Pilihan tepat |
|---|---|
| Rekomendasi produk gagal | Fallback: produk terlaris dari cache |
| Estimasi ongkir gagal | Fallback: tampilkan "belum tersedia", checkout ditunda |
| Fraud check gagal | Gagal cepat — jangan anggap aman |
| Saldo/stok tidak bisa dicek | Gagal cepat dengan pesan jelas |
Untuk data yang menyangkut uang, keamanan, atau stok, mengarang jawaban lebih berbahaya daripada menolak permintaan.
Retry storm, backoff, dan jitter
Retry membantu untuk gangguan sesaat, tapi bisa menjadi senjata makan tuan. Bila tiga lapisan masing-masing me-retry 3 kali, satu permintaan pengguna bisa berubah menjadi 3 × 3 × 3 = 27 panggilan ke database yang sedang sakit. Bila semua client me-retry dengan jeda tetap, mereka menghantam service bersamaan tepat saat service mencoba pulih — retry storm.
Obatnya: retry hanya di satu lapisan, batasi jumlah percobaan, pakai backoff eksponensial, dan tambahkan jitter agar waktu retry tersebar.
java
import java.time.Duration;
import java.util.Random;
public class RetryBackoff {
// Full jitter: tunggu acak antara 0 dan min(cap, base * 2^attempt)
static Duration fullJitter(int attempt, Duration base, Duration cap, Random random) {
long exp = base.toMillis() * (1L << attempt);
long ceiling = Math.min(cap.toMillis(), exp);
return Duration.ofMillis(random.nextLong(ceiling + 1));
}
public static void main(String[] args) {
Duration base = Duration.ofMillis(100);
Duration cap = Duration.ofSeconds(2);
Random random = new Random(42); // seed tetap agar output bisa diulang
for (int attempt = 0; attempt < 5; attempt++) {
long ceiling = Math.min(cap.toMillis(), base.toMillis() * (1L << attempt));
Duration wait = fullJitter(attempt, base, cap, random);
System.out.printf("percobaan %d: batas %4d ms, tunggu %4d ms%n",
attempt + 1, ceiling, wait.toMillis());
}
}
}text
percobaan 1: batas 100 ms, tunggu 2 ms
percobaan 2: batas 200 ms, tunggu 106 ms
percobaan 3: batas 400 ms, tunggu 260 ms
percobaan 4: batas 800 ms, tunggu 517 ms
percobaan 5: batas 1600 ms, tunggu 1006 msDi aplikasi nyata kamu tidak menulis loop ini sendiri — Resilience4j Retry atau @Retryable bawaan Spring Framework 7 sudah menyediakan backoff dan jitter. Yang penting kamu paham kenapa angkanya harus acak dan dibatasi.
Idempotensi
Timeout bukan bukti gagal: permintaan bisa saja sudah diproses, hanya responsnya yang hilang. Karena itu retry hanya aman untuk operasi idempoten — dieksekusi berkali-kali hasilnya sama. GET dan PUT dengan nilai absolut biasanya idempoten; POST /payments tidak. Solusi umumnya adalah idempotency key: client mengirim header unik per transaksi, server menyimpan hasil per key dan mengembalikan hasil yang sama bila key itu datang lagi.
Chaos engineering sekilas
Pola di atas baru terbukti saat benar-benar diuji. Chaos engineering adalah eksperimen terkontrol: sengaja menambah latensi, mematikan pod, atau memutus jaringan ke satu dependency, lalu mengamati apakah sistem berperilaku sesuai hipotesis. Mulailah di staging, dengan cakupan kecil dan tombol berhenti yang jelas.
Contoh di dunia backend
Checkout e-commerce memanggil tiga dependency dengan kebijakan berbeda sesuai tingkat kritisnya:
java
// Potongan — checkout dengan kebijakan resilience per dependency
@Service
public class CheckoutService {
private final CircuitBreakerFactory<?, ?> breakers;
private final InventoryApi inventory;
private final ShippingApi shipping;
private final PaymentApi payment;
public CheckoutService(CircuitBreakerFactory<?, ?> breakers, InventoryApi inventory,
ShippingApi shipping, PaymentApi payment) {
this.breakers = breakers;
this.inventory = inventory;
this.shipping = shipping;
this.payment = payment;
}
public CheckoutResult checkout(Order order, String idempotencyKey) {
// Stok kritis: gagal cepat, tanpa fallback yang mengarang
boolean available = breakers.create("inventory").run(
() -> inventory.reserve(order.items()),
ex -> { throw new ServiceUnavailableException("Stok belum bisa dicek, coba lagi"); });
if (!available) {
return CheckoutResult.outOfStock();
}
// Ongkir tidak kritis: fallback jujur
ShippingQuote quote = breakers.create("shipping").run(
() -> shipping.quote(order.address()),
ex -> ShippingQuote.pending());
// Pembayaran: idempotency key agar retry tidak menagih dua kali
PaymentReceipt receipt = breakers.create("payment").run(
() -> payment.charge(order.total(), idempotencyKey),
ex -> { throw new ServiceUnavailableException("Pembayaran sedang gangguan"); });
return CheckoutResult.success(receipt, quote);
}
}Masing-masing nama breaker (inventory, shipping, payment) punya konfigurasi sendiri di application.yml, sehingga gangguan ongkir tidak membuka breaker pembayaran, dan timeout tiap breaker disusun agar totalnya muat dalam budget yang diberikan gateway.
Pertanyaan interview
1. Apa itu cascading failure dan bagaimana mencegahnya?
Cascading failure terjadi saat satu dependency lambat atau gagal menyebabkan pemanggilnya kehabisan sumber daya, lalu merambat ke lapisan di atasnya. Pencegahannya: timeout yang ketat dan berantai sesuai budget, circuit breaker per dependency untuk gagal cepat, bulkhead untuk membatasi sumber daya per dependency, dan load shedding saat service sendiri kelebihan beban.
2. Kenapa retry bisa memperburuk insiden, dan bagaimana cara aman memakainya?
Retry melipatgandakan beban tepat saat dependency sedang sakit, apalagi bila dilakukan di banyak lapisan dan dengan jeda tetap sehingga client tersinkron (retry storm). Cara aman: retry hanya di satu lapisan, batasi percobaan, gunakan backoff eksponensial dengan jitter, hanya untuk error sementara, dan hanya untuk operasi idempoten.
3. Kapan memakai fallback dan kapan gagal cepat?
Fallback cocok untuk fitur non-kritis yang punya jawaban pengganti yang jujur, seperti rekomendasi dari cache. Untuk keputusan kritis — pembayaran, fraud check, stok, otorisasi — gagal cepat dengan pesan jelas lebih aman, karena fallback yang mengarang jawaban bisa menyebabkan kerugian uang atau celah keamanan.
Jebakan umum
- Memanggil service lain tanpa timeout, atau memakai timeout lapisan dalam yang lebih besar dari lapisan luar.
- Memakai satu circuit breaker untuk semua dependency sehingga satu gangguan mematikan semua panggilan.
- Retry di setiap lapisan dengan jeda tetap — resep retry storm.
- Me-retry
POSTyang tidak idempoten sehingga pesanan atau tagihan tercipta dua kali. - Fallback yang menyembunyikan kegagalan kritis, misalnya menganggap fraud check lulus.
- Lupa bahwa time limiter Resilience4j default-nya 1 detik saat memakai Spring Cloud Circuit Breaker.
Ringkasan
- Kegagalan merambat karena menunggu terlalu lama dan tidak ada batas sumber daya per dependency.
- Timeout berantai mengikuti deadline: setiap langkah memakai
min(sisa budget, batas per dependency). - Circuit breaker per dependency (
CLOSED/OPEN/HALF_OPEN) dan bulkhead mengisolasi gangguan; Spring Cloud Circuit Breaker + Resilience4j menyediakannya lewat konfigurasi. - Load shedding (
503) dan rate limit (429) melindungi service dari kelebihan beban. - Retry hanya di satu lapisan, dengan backoff eksponensial, jitter, batas percobaan, dan hanya untuk operasi idempoten.
- Fallback harus jujur dan aman; untuk uang, keamanan, dan stok, pilih gagal cepat. Uji semuanya dengan chaos engineering terkontrol.