Sistem production yang baik bukan yang tidak pernah gagal — melainkan yang gagal dengan anggun. Resilience patterns adalah teknik untuk mencegah satu komponen yang bermasalah meruntuhkan seluruh sistem.
Masalah: Cascade Failure
Skenario klasik: service Payment lambat (bukan mati). Client me-retry → lebih banyak request antri → connection pool penuh → service Order yang memanggil Payment jadi lambat → service API Gateway yang memanggil Order jadi lambat → seluruh sistem tumbang karena satu dependency.
Empat pattern ini mencegah skenario itu.
Pattern 1: Circuit Breaker
Inspirasi: circuit breaker listrik yang trip saat arus berlebih.
Tiga state:
┌─── CLOSED ─────┐
│ Request lewat │ (normal)
│ Hitung error │
└────────┬───────┘
│ error rate > threshold
▼
┌─── OPEN ───────┐
│ Semua request │ (fail fast, jangan hit service)
│ langsung gagal │
└────────┬───────┘
│ setelah timeout (mis. 30s)
▼
┌── HALF-OPEN ───┐
│ Satu request │ (probe: apakah service sudah pulih?)
│ coba pass │
└────────┬───────┘
sukses │ gagal
├──────────► OPEN
▼
CLOSED
Pseudocode sederhana:
class CircuitBreaker {
state = "CLOSED";
failureCount = 0;
failureThreshold = 5;
timeout = 30_000; // ms
nextAttempt = 0;
async call(fn) {
if (this.state === "OPEN") {
if (Date.now() < this.nextAttempt) {
throw new Error("Circuit OPEN — fail fast");
}
this.state = "HALF_OPEN";
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (err) {
this.onFailure();
throw err;
}
}
onSuccess() {
this.failureCount = 0;
this.state = "CLOSED";
}
onFailure() {
this.failureCount++;
if (this.failureCount >= this.failureThreshold) {
this.state = "OPEN";
this.nextAttempt = Date.now() + this.timeout;
}
}
}
Keuntungan: service yang sakit tidak terus diserang request, sempat pulih. Client dapat response gagal cepat (bukan timeout 30 detik).
Real world: Netflix Hystrix (sudah deprecated, digantikan Resilience4j). Library modern: Resilience4j (Java), Polly (.NET), opossum (Node.js).
Pattern 2: Bulkhead Isolation
Inspirasi: kapal laut dibagi jadi beberapa compartment (bulkhead) — kalau satu bocor, kapal tetap mengapung.
Isolasi resource per dependency supaya kegagalan tidak merembet.
Tanpa bulkhead:
Semua request share 1 thread pool 100 thread.
Service A lambat → 80 thread stuck di A.
Service B (sehat) cuma dapat 20 thread → B jadi ikut lambat.
Dengan bulkhead:
Service A: thread pool dedicated 30 thread.
Service B: thread pool dedicated 30 thread.
Service C: thread pool dedicated 30 thread.
Pool A penuh → hanya request ke A yang gagal. B dan C tetap jalan.
Implementasi bisa via:
- Thread pool terpisah per downstream service.
- Semaphore / connection pool terpisah untuk async I/O.
- Container / pod terpisah di Kubernetes.
Kombinasi dengan circuit breaker sangat kuat: bulkhead mencegah resource exhaustion, circuit breaker menghentikan traffic ke service yang sakit.
Pattern 3: Timeout & Deadline Propagation
Setiap call network wajib punya timeout. Tanpa timeout, satu call lambat bisa bikin thread stuck selamanya.
Cascading Timeout Budget
Kalau request user punya SLA 500ms, jangan set timeout 5 detik di setiap call internal — atur budget yang cascade:
Client → API Gateway (budget: 500ms)
→ Order Service (budget: 400ms, sisakan 100ms buffer)
→ Payment (budget: 200ms)
→ Inventory (budget: 150ms)
Kalau Payment > 200ms, Order Service batalkan (fail fast) daripada client menunggu percuma.
Deadline Propagation
Di sistem modern (gRPC, OpenTelemetry), deadline di-pass dalam context:
// Pseudocode — pass deadline ke downstream call
async function handleRequest(ctx, req) {
const deadline = ctx.deadline ?? Date.now() + 500;
const remaining = deadline - Date.now();
if (remaining <= 0) throw new Error("Deadline exceeded");
return await callDownstream(req, { timeout: remaining - 50 }); // sisakan buffer
}
Setiap service tahu berapa waktu tersisa, dan bisa memutuskan untuk tidak memulai operasi mahal yang dipastikan tidak akan selesai.
Pattern 4: Retry with Exponential Backoff + Jitter
Sudah kita bahas di lesson idempotency — tapi penting ditegaskan: retry tanpa backoff = DDoS service sendiri.
Full jitter formula (rekomendasi AWS):
delay = random(0, min(cap, base * 2^attempt))
base= delay awal (mis. 100ms)cap= maksimum delay (mis. 30s)attempt= nomor percobaan
Jitter mencegah retry-storm saat service baru pulih. Kalau 1000 client retry bersamaan tanpa jitter, service langsung tumbang lagi.
Pattern 5: Graceful Degradation & Fallback
Saat dependency gagal, jangan tampilkan error page — degrade gracefully:
- Rekomendasi produk tidak tersedia? Tampilkan default populer.
- Review service down? Sembunyikan section review, sisa halaman normal.
- Pricing service lambat? Pakai cache 5 menit, tandai "prices may be slightly outdated".
- Search service mati? Fallback ke listing berdasarkan popularity.
Prinsip: fail fast, but serve something useful. User lebih puas melihat 80% fitur yang jalan daripada error 500.
Fail Fast vs Fail Slow
- Fail fast — kalau dependency pasti gagal (circuit breaker OPEN, timeout sudah habis), gagal sekarang dengan pesan jelas. Bebaskan resource.
- Fail slow — biarkan request menggantung, retry diam-diam, pakai resource. Hampir selalu buruk.
Bedanya besar: fail fast memungkinkan user atau upstream service memutuskan tindakan (retry, switch to fallback, tampilkan error). Fail slow hanya menumpuk request.
Kombinasi di Production
Stack resilience real-world:
Request masuk
↓
[Rate Limiter] — batasi request per user
↓
[Timeout 500ms] — kalau lebih lambat, abort
↓
[Circuit Breaker] — skip service yang sakit
↓
[Bulkhead pool] — isolasi resource per dependency
↓
[Retry + jitter] — untuk error transient yang bisa pulih
↓
[Fallback logic] — kalau tetap gagal, tampilkan sesuatu yang berguna
Netflix menjalankan ini di skala miliaran request/hari. Mereka bahkan membangun Chaos Monkey — tool yang sengaja mematikan service acak di production untuk memverifikasi resilience pattern bekerja.
Yang Perlu Diingat
Resilience bukan fitur tambahan — ia harus dibangun sejak awal, karena retrofitting di sistem yang sudah kompleks jauh lebih sulit. Setiap call network adalah potensi kegagalan: timeout wajib, circuit breaker untuk dependency kritis, fallback kalau bisa. Aturan praktis: asumsikan semua yang melewati network akan gagal, dan desainlah untuk itu.
🎭 Analogi sehari-hari: Sirkuit listrik rumah. Korslet di stop kontak kamar mandi → MCB jeglek (circuit breaker open). Mau pasang lagi pas service tukang datang → coba colok satu alat (half-open). Aman? MCB tutup balik (closed). Gak aman? Tetap jeglek (open). Sirkuit melindungi seluruh rumah dari satu masalah lokal.
💡 4 resilience pattern bekerja bersama:
- Timeout = batas tunggu tiap call
- Retry + jitter = transient recovery tanpa thundering herd
- Circuit breaker = stop call ke service yang persistent fail
- Bulkhead = isolate resource pool antar dependency
Masing-masing pattern fix masalah berbeda. Stack semuanya = system tahan banting.
⚠️ Jebakan klasik:
- Retry tanpa circuit breaker = service down → retry storm → service makin down
- Circuit breaker terlalu sensitif = false trip, service sehat dianggap mati
- Timeout terlalu longgar = thread pool penuh, cascade failure
- Bulkhead bocor = shared connection pool antar service = isolation gagal
- No fallback saat circuit open = user dapat error mentah, lebih baik kasih cached/degraded response
🎯 Default config aman:
- Timeout: p99 latency × 2 (e.g., normal 200ms → timeout 400ms)
- Retry: max 3 attempts, exponential backoff 100ms→200ms→400ms + full jitter
- Circuit breaker: trip kalau error rate > 50% dalam 10 detik, recovery 30 detik
- Bulkhead: thread/connection pool terpisah per dependency kritis
TL;DR: Resilience = gagal dengan anggun. 4 pattern: Timeout + Retry+Jitter + Circuit Breaker + Bulkhead. Stack untuk cegah cascade failure. Library: Resilience4j (Java), Polly (.NET), opossum (Node.js).