Resilience Patterns — System Design

Sistem production yang baik bukan yang tidak pernah gagal — melainkan yang gagal dengan anggun. Resilience patterns adalah teknik untuk mencegah satu komponen y

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:

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))

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:

Prinsip: fail fast, but serve something useful. User lebih puas melihat 80% fitur yang jalan daripada error 500.

Fail Fast vs Fail Slow

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.

<title>Circuit Breaker States</title> Circuit Breaker — 3 State Machine CLOSED request lewat hitung errors OPEN fail fast, skip call HALF-OPEN satu probe untuk test error rate > threshold timeout berlalu probe sukses probe gagal State berpindah berdasarkan error count, timer, atau hasil probe. Breaker yang OPEN membebaskan resource dan memberi waktu service untuk pulih.
Circuit breaker transisi antara CLOSED (normal), OPEN (fail fast), dan HALF-OPEN (probe). Trigger: error rate melebihi threshold, timeout expired, atau hasil probe.

🎭 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:

Masing-masing pattern fix masalah berbeda. Stack semuanya = system tahan banting.

⚠️ Jebakan klasik:

🎯 Default config aman:

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).

Yang akan kamu pelajari