Circuit Breaker #

Bayangkan sistem e-commerce yang bergantung pada payment gateway eksternal. Suatu hari, payment gateway mengalami gangguan dan setiap request ke sana butuh 30 detik sebelum timeout. Tanpa mekanisme perlindungan, semua thread yang menangani checkout akan tersandera menunggu timeout — 30 detik × ribuan request bersamaan — dan seluruh aplikasi menjadi tidak responsif meski hanya satu dependency yang bermasalah.

Circuit breaker adalah pattern yang terinspirasi dari dunia kelistrikan: sebuah pengaman yang otomatis memutus sirkuit ketika mendeteksi beban berlebih, mencegah kerusakan yang lebih parah ke sistem yang lebih luas. Dalam software, circuit breaker memantau kegagalan terhadap suatu dependency — database, API eksternal, atau service internal — dan ketika kegagalan melampaui threshold, ia “membuka” sirkuit: request langsung gagal dengan cepat tanpa menunggu timeout, memberikan waktu bagi dependency untuk pulih.

Pattern ini adalah salah satu fondasi dari sistem yang resilient — sistem yang bisa menghadapi kegagalan parsial tanpa kegagalan total.

Tiga State Circuit Breaker #

Circuit breaker beroperasi dalam tiga state yang berpindah secara otomatis berdasarkan kondisi dependency.

stateDiagram-v2
    [*] --> Closed

    Closed --> Open : Failure threshold tercapai\n(misalnya: 5 gagal dalam 60 detik)
    Open --> HalfOpen : Recovery timeout berlalu\n(misalnya: setelah 30 detik)
    HalfOpen --> Closed : Probe request berhasil\n(misalnya: 2 success berturut-turut)
    HalfOpen --> Open : Probe request gagal\n(langsung kembali Open)

    state Closed {
        [*]: Request diizinkan\nKegagalan dihitung
    }
    state Open {
        [*]: Request langsung ditolak\nTidak ada call ke dependency
    }
    state HalfOpen {
        [*]: Beberapa request diizinkan\nsebagai probe
    }
Detail setiap state:

  CLOSED (Normal):
  → Semua request diteruskan ke dependency
  → Setiap kegagalan dicatat
  → Jika failure rate melampaui threshold → pindah ke OPEN
  → Jika dalam window waktu kegagalan tidak cukup → counter direset

  OPEN (Sirkuit Terbuka — Melindungi):
  → Semua request langsung gagal (fast fail)
  → Tidak ada request yang dikirim ke dependency
  → Setelah recovery_timeout berlalu → pindah ke HALF-OPEN
  → Manfaat: latency rendah (tidak ada timeout), dependency punya waktu pulih

  HALF-OPEN (Probe):
  → Beberapa request probe dikirim ke dependency
  → Jika berhasil → pindah ke CLOSED (sistem normal)
  → Jika gagal → kembali ke OPEN (dependency belum pulih)
  → Mencegah sistem membanjiri dependency yang baru mulai pulih

Implementasi dari Scratch #

Memahami implementasi dari awal membantu engineer mengkonfigurasi dan men-debug circuit breaker dengan lebih baik.

// Circuit breaker dengan sliding window untuk failure tracking.
// Thread-safe untuk penggunaan concurrent.

type CircuitState int

const (
    Closed CircuitState = iota
    Open
    HalfOpen
)

// ErrCircuitOpen dikembalikan ketika circuit breaker dalam state OPEN.
var ErrCircuitOpen = errors.New("circuit is OPEN")

type CircuitBreakerConfig struct {
    // Berapa kegagalan dalam window sebelum OPEN
    FailureThreshold int
    // Window waktu untuk menghitung kegagalan
    FailureWindow time.Duration
    // Berapa lama di state OPEN sebelum coba HALF-OPEN
    RecoveryTimeout time.Duration
    // Berapa success di HALF-OPEN sebelum kembali CLOSED
    SuccessThreshold int
    // Berapa request yang diizinkan di HALF-OPEN per window
    HalfOpenMaxCalls int
    // Error apa saja yang dihitung sebagai kegagalan (nil = semua error)
    IsFailure func(err error) bool
}

type CircuitBreaker struct {
    name   string
    config CircuitBreakerConfig
    mu     sync.Mutex

    state CircuitState
    // Sliding window: simpan timestamp setiap kegagalan
    failureTimestamps []time.Time
    successCount      int
    halfOpenCalls     int
    openedAt          time.Time

    // Callback untuk observability
    OnStateChange func(name string, from, to CircuitState)
}

func NewCircuitBreaker(name string, config CircuitBreakerConfig) *CircuitBreaker {
    return &CircuitBreaker{name: name, config: config, state: Closed}
}

func (cb *CircuitBreaker) Call(fn func() error) error {
    cb.mu.Lock()
    cb.evaluateState()

    if cb.state == Open {
        cb.mu.Unlock()
        return ErrCircuitOpen
    }
    if cb.state == HalfOpen {
        if cb.halfOpenCalls >= cb.config.HalfOpenMaxCalls {
            cb.mu.Unlock()
            return ErrCircuitOpen
        }
        cb.halfOpenCalls++
    }
    cb.mu.Unlock()

    // Eksekusi di luar lock agar goroutine lain tidak terblokir
    err := fn()
    if err != nil {
        if cb.config.IsFailure == nil || cb.config.IsFailure(err) {
            cb.recordFailure()
        }
        return err
    }
    cb.recordSuccess()
    return nil
}

func (cb *CircuitBreaker) evaluateState() {
    // Cek apakah state perlu diubah berdasarkan kondisi saat ini.
    if cb.state == Open && cb.shouldAttemptRecovery() {
        cb.transitionTo(HalfOpen)
    }
}

func (cb *CircuitBreaker) recordSuccess() {
    cb.mu.Lock()
    defer cb.mu.Unlock()

    if cb.state == HalfOpen {
        cb.successCount++
        if cb.successCount >= cb.config.SuccessThreshold {
            cb.transitionTo(Closed)
        }
    }
}

func (cb *CircuitBreaker) recordFailure() {
    cb.mu.Lock()
    defer cb.mu.Unlock()

    now := time.Now()
    cb.failureTimestamps = append(cb.failureTimestamps, now)

    // Hapus failure yang sudah di luar window
    cutoff := now.Add(-cb.config.FailureWindow)
    for len(cb.failureTimestamps) > 0 && cb.failureTimestamps[0].Before(cutoff) {
        cb.failureTimestamps = cb.failureTimestamps[1:]
    }

    if cb.state == HalfOpen {
        // Langsung kembali ke OPEN jika probe gagal
        cb.transitionTo(Open)
    } else if cb.state == Closed && len(cb.failureTimestamps) >= cb.config.FailureThreshold {
        cb.transitionTo(Open)
    }
}

func (cb *CircuitBreaker) transitionTo(newState CircuitState) {
    oldState := cb.state
    cb.state = newState

    switch newState {
    case Open:
        cb.openedAt = time.Now()
        cb.successCount = 0
        cb.halfOpenCalls = 0
    case Closed:
        cb.failureTimestamps = nil
        cb.successCount = 0
        cb.halfOpenCalls = 0
    case HalfOpen:
        cb.successCount = 0
        cb.halfOpenCalls = 0
    }

    if cb.OnStateChange != nil && oldState != newState {
        cb.OnStateChange(cb.name, oldState, newState)
    }
}

func (cb *CircuitBreaker) shouldAttemptRecovery() bool {
    if cb.openedAt.IsZero() {
        return false
    }
    return time.Since(cb.openedAt) >= cb.config.RecoveryTimeout
}

// FailureCount mengembalikan jumlah kegagalan dalam window saat ini.
func (cb *CircuitBreaker) FailureCount() int {
    cb.mu.Lock()
    defer cb.mu.Unlock()

    now := time.Now()
    cutoff := now.Add(-cb.config.FailureWindow)
    count := 0
    for _, ts := range cb.failureTimestamps {
        if !ts.Before(cutoff) {
            count++
        }
    }
    return count
}
#

Penggunaan di Aplikasi Nyata #

// Setup circuit breaker dengan callback untuk monitoring
var paymentBreaker = NewCircuitBreaker("payment-gateway", CircuitBreakerConfig{
    FailureThreshold: 5,
    FailureWindow:    60 * time.Second,
    RecoveryTimeout:  30 * time.Second,
    SuccessThreshold: 2,
})

func init() {
    paymentBreaker.OnStateChange = func(name string, from, to CircuitState) {
        log.Printf("Circuit breaker state change: %s %v -> %v", name, from, to)
        // Kirim metric ke monitoring
        metrics.Increment("circuit_breaker.state_change", map[string]string{
            "circuit": name,
            "state":   fmt.Sprint(to),
        })
    }
}

// Fungsi yang dilindungi circuit breaker
func chargePayment(orderID string, amount float64) (PaymentResult, error) {
    var result PaymentResult
    err := paymentBreaker.Call(func() error {
        resp, err := http.Post(
            "https://payment-gateway.com/charge",
            "application/json",
            strings.NewReader(fmt.Sprintf(`{"order_id": %q, "amount": %f}`, orderID, amount)),
        )
        if err != nil {
            return err
        }
        defer resp.Body.Close()
        if resp.StatusCode >= 400 {
            return fmt.Errorf("payment gateway returned %d", resp.StatusCode)
        }
        return json.NewDecoder(resp.Body).Decode(&result)
    })
    if err != nil {
        if errors.Is(err, ErrCircuitOpen) {
            // Circuit terbuka — gunakan fallback
            log.Printf("Payment circuit open for order %s", orderID)
            return fallbackPaymentHandler(orderID, amount)
        }
        // Request gagal (circuit masih closed/half-open, kegagalan dicatat)
        log.Printf("Payment request failed for order %s: %v", orderID, err)
        return PaymentResult{}, fmt.Errorf("payment processing failed: %w", err)
    }
    return result, nil
}

func fallbackPaymentHandler(orderID string, amount float64) (PaymentResult, error) {
    // Fallback ketika payment gateway tidak tersedia.
    // Opsi:
    // 1. Queue untuk diproses nanti (async)
    // 2. Coba payment provider alternatif
    // 3. Return error yang informatif ke user
    // Opsi 1: Queue ke background job
    queuePendingPayment(orderID, amount)
    return PaymentResult{
        Status:  "queued",
        Message: "The payment will be processed in a few minutes",
        OrderID: orderID,
    }, nil
}
#

Library Circuit Breaker yang Tersedia #

Untuk production, sebaiknya gunakan library yang sudah teruji daripada implementasi sendiri.


# Python: pybreaker
from pybreaker import CircuitBreaker, CircuitBreakerError

payment_breaker = CircuitBreaker(
    fail_max=5,
    reset_timeout=30,
    exclude=[ValueError]  # exception ini tidak dihitung sebagai failure
)

@payment_breaker
def call_payment_api(order_id: str, amount: float):
    response = httpx.post("https://payment-gateway.com/charge", ...)
    return response.json()

try:
    result = call_payment_api(order_id, amount)
except CircuitBreakerError:
    # Circuit terbuka
    handle_payment_unavailable(order_id)

// Go: sony/gobreaker
package main

import (
    "github.com/sony/gobreaker"
    "time"
)

var paymentBreaker = gobreaker.NewCircuitBreaker(gobreaker.Settings{
    Name:        "payment-gateway",
    MaxRequests: 2,         // max request di HALF-OPEN
    Interval:    60 * time.Second,   // window untuk reset counter
    Timeout:     30 * time.Second,   // recovery timeout (OPEN → HALF-OPEN)
    ReadyToTrip: func(counts gobreaker.Counts) bool {
        // Custom logic: buka circuit jika failure rate > 60% dengan min 5 request
        if counts.Requests < 5 {
            return false
        }
        failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
        return failureRatio >= 0.6
    },
    OnStateChange: func(name string, from, to gobreaker.State) {
        log.Printf("Circuit %s: %s → %s", name, from, to)
    },
})

func chargePayment(orderID string, amount float64) (PaymentResult, error) {
    result, err := paymentBreaker.Execute(func() (interface{}, error) {
        return callPaymentAPI(orderID, amount)
    })

    if err != nil {
        if err == gobreaker.ErrOpenState {
            return PaymentResult{}, ErrPaymentUnavailable
        }
        return PaymentResult{}, err
    }

    return result.(PaymentResult), nil
}
// Bulkhead: isolasi worker pool per dependency.
// Mencegah satu dependency lambat menghabiskan semua goroutine.

type BulkheadExecutor struct {
    name    string
    sem     chan struct{}   // channel buffer = semaphore
    workers chan func()
    wg      sync.WaitGroup
}

func NewBulkheadExecutor(name string, maxWorkers, queueSize int) *BulkheadExecutor {
    b := &BulkheadExecutor{
        name:    name,
        sem:     make(chan struct{}, maxWorkers+queueSize),
        workers: make(chan func(), queueSize),
    }
    for i := 0; i < maxWorkers; i++ {
        b.wg.Add(1)
        go func() {
            defer b.wg.Done()
            for fn := range b.workers {
                fn()
            }
        }()
    }
    return b
}

// Submit task ke pool yang terisolasi; mengembalikan channel berisi hasil.
func (b *BulkheadExecutor) Submit(fn func() interface{}) (chan interface{}, error) {
    select {
    case b.sem <- struct{}{}:  // acquire
    default:
        return nil, ErrBulkheadFull
    }

    resultCh := make(chan interface{}, 1)
    b.workers <- func() {
        defer func() { <-b.sem }()  // release
        resultCh <- fn()
    }
    return resultCh, nil
}

// Setup: setiap dependency punya worker pool sendiri
paymentPool := NewBulkheadExecutor("payment", 10, 5)
inventoryPool := NewBulkheadExecutor("inventory", 20, 10)
emailPool := NewBulkheadExecutor("email", 5, 50)

// Jika payment API lambat, hanya 10 worker yang terpengaruh
func processCheckout(order Order) (interface{}, error) {
    paymentCh, err := paymentPool.Submit(func() interface{} {
        return chargePayment(order)
    })
    if err != nil {
        return map[string]string{"error": "Service busy, please try again"}, nil
    }
    inventoryCh, _ := inventoryPool.Submit(func() interface{} {
        return reserveInventory(order)
    })

    select {
    case paymentResult := <-paymentCh:
        inventoryResult := <-inventoryCh
        return finalizeOrder(order, paymentResult, inventoryResult), nil
    case <-time.After(15 * time.Second):
        return nil, errors.New("payment timeout")
    }
}
#

Konfigurasi Threshold yang Tepat #

Konfigurasi yang salah bisa membuat circuit breaker tidak efektif atau terlalu agresif.

Panduan memilih threshold:

  failure_threshold (berapa gagal sebelum OPEN):
  → Terlalu rendah (1-2): circuit terbuka untuk fluke error, terlalu agresif
  → Terlalu tinggi (50+): terlambat mendeteksi masalah nyata
  → Rekomendasi: 5-10 kegagalan dalam window 60 detik
  → Atau: failure rate (50-60%) dengan minimum volume request

  failure_window (berapa lama menghitung kegagalan):
  → Terlalu pendek (5 detik): kegagalan lama masih menumpuk
  → Terlalu panjang (10 menit): terlambat reset setelah masalah selesai
  → Rekomendasi: 60 detik untuk kebanyakan kasus

  recovery_timeout (berapa lama OPEN sebelum probe):
  → Sesuaikan dengan waktu recovery yang diharapkan dari dependency
  → Database restart: ~30 detik
  → External API: ~60 detik (beri waktu mereka menyadari masalah)
  → Rekomendasi: mulai dari 30-60 detik, adjust berdasarkan SLA dependency

  success_threshold (berapa success sebelum CLOSED):
  → Terlalu rendah (1): bisa langsung CLOSED meski belum stabil
  → Rekomendasi: 2-3 untuk confidence yang cukup

  Jangan gunakan angka yang sama untuk semua:
  → Payment API yang kritis: threshold lebih ketat, recovery lebih hati-hati
  → Cache layer yang optional: threshold lebih longgar
  → Internal service yang cepat: window yang lebih pendek

Bulkhead Pattern: Melengkapi Circuit Breaker #

Circuit breaker melindungi dari cascade failure karena kualitas yang buruk. Bulkhead melindungi dari cascade failure karena kuantitas yang berlebihan — mencegah satu dependency yang lambat menghabiskan semua thread/connection.

// Metrics yang perlu di-expose untuk setiap circuit breaker

type MonitoredCircuitBreaker struct {
    *CircuitBreaker
    // Prometheus counters dan gauges
    requestsTotal     *prometheus.CounterVec  // labels: circuit, result
    stateGauge        *prometheus.GaugeVec    // labels: circuit
    failureCountGauge *prometheus.GaugeVec    // labels: circuit
}

func NewMonitoredCircuitBreaker(name string, config CircuitBreakerConfig) *MonitoredCircuitBreaker {
    m := &MonitoredCircuitBreaker{
        CircuitBreaker: NewCircuitBreaker(name, config),
        requestsTotal: prometheus.NewCounterVec(
            prometheus.CounterOpts{
                Name: "circuit_breaker_requests_total",
                Help: "Total requests through circuit breaker",
            },
            []string{"circuit", "result"},  // result: success, failure, rejected
        ),
        stateGauge: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "circuit_breaker_state",
                Help: "Current state (0=closed, 1=open, 2=half_open)",
            },
            []string{"circuit"},
        ),
        failureCountGauge: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "circuit_breaker_failure_count",
                Help: "Current failure count in window",
            },
            []string{"circuit"},
        ),
    }
    m.OnStateChange = m.updateStateMetric
    return m
}

func (m *MonitoredCircuitBreaker) updateStateMetric(name string, from, to CircuitState) {
    stateMap := map[CircuitState]float64{
        Closed: 0, Open: 1, HalfOpen: 2,
    }
    m.stateGauge.WithLabelValues(name).Set(stateMap[to])
}

func (m *MonitoredCircuitBreaker) Call(fn func() error) error {
    defer func() {
        m.failureCountGauge.WithLabelValues(m.name).
            Set(float64(m.FailureCount()))
    }()

    err := m.CircuitBreaker.Call(fn)
    if err != nil {
        if errors.Is(err, ErrCircuitOpen) {
            m.requestsTotal.WithLabelValues(m.name, "rejected").Inc()
        } else {
            m.requestsTotal.WithLabelValues(m.name, "failure").Inc()
        }
        return err
    }
    m.requestsTotal.WithLabelValues(m.name, "success").Inc()
    return nil
}
#

Monitoring Circuit Breaker #

Circuit breaker tanpa monitoring adalah circuit breaker yang tidak berguna — kamu tidak akan tahu kapan dan mengapa sirkuit terbuka.

// Padanan Go: breaker generik dengan API berbasis event yang sama
var paymentBreaker = NewCircuitBreaker("payment-gateway", CircuitBreakerConfig{
    FailureThreshold: 5,                // volume threshold: min request sebelum menghitung error rate
    FailureWindow:    60 * time.Second, // window untuk reset counter
    RecoveryTimeout:  30 * time.Second, // resetTimeout: coba lagi setelah 30 detik
    SuccessThreshold: 2,
})

// Event listener untuk monitoring
paymentBreaker.OnStateChange = func(name string, from, to CircuitState) {
    switch to {
    case Open:
        log.Println("Payment circuit OPENED")
        metrics.Increment("circuit.opened", map[string]string{"circuit": "payment"})
    case HalfOpen:
        log.Println("Payment circuit HALF-OPEN — probing")
    case Closed:
        log.Println("Payment circuit CLOSED — recovered")
    }
}

// Fallback: dipanggil saat circuit OPEN
func chargePayment(orderID string, amount float64) (PaymentResult, error) {
    var result PaymentResult
    err := paymentBreaker.Call(func() error {
        // timeout: 10000 (timeout per request) ditangani oleh HTTP client
        var err error
        result, err = callPaymentAPI(orderID, amount)
        return err
    })
    if err != nil {
        if errors.Is(err, ErrCircuitOpen) {
            return queuePaymentForLater(orderID, amount)
        }
        return PaymentResult{}, err
    }
    return result, nil
}

// Penggunaan
func checkoutHandler(w http.ResponseWriter, r *http.Request) {
    orderID, amount := parseCheckout(r)

    result, err := chargePayment(orderID, amount)
    if err != nil {
        // Circuit terbuka atau request gagal
        w.WriteHeader(http.StatusServiceUnavailable)
        json.NewEncoder(w).Encode(map[string]string{
            "error": "Payment service temporarily unavailable",
        })
        return
    }
    json.NewEncoder(w).Encode(result)
}
Alert yang perlu dipasang:

  Alert kritis:
  → Circuit OPEN untuk dependency yang critical path
    circuit_breaker_state{circuit="payment"} == 1
    → PagerDuty/on-call engineer

  Alert warning:
  → Circuit OPEN untuk dependency yang non-critical
    circuit_breaker_state{circuit="recommendation"} == 1
    → Slack notification

  → Failure rate naik mendekati threshold
    circuit_breaker_failure_count > threshold * 0.8
    → Early warning sebelum circuit terbuka

  Dashboard yang berguna:
  → State setiap circuit breaker (closed/open/half-open)
  → Failure rate per circuit per waktu
  → Jumlah request yang di-reject (circuit open)
  → Durasi circuit dalam state OPEN

Anti-Pattern yang Harus Dihindari #

// ✗ Anti-pattern 1: Circuit breaker yang tidak di-configure per dependency
// Satu circuit breaker global untuk semua call
globalBreaker := NewCircuitBreaker("global", defaultConfig)  // JANGAN

// ✓ Solusi: circuit breaker terpisah per dependency
paymentBreaker := NewCircuitBreaker("payment-gateway", defaultConfig)
inventoryBreaker := NewCircuitBreaker("inventory-service", defaultConfig)
emailBreaker := NewCircuitBreaker("email-service", defaultConfig)

// ✗ Anti-pattern 2: Tidak ada fallback ketika circuit OPEN
func getProduct(productID string) (Product, error) {
    var product Product
    err := productBreaker.Call(func() error {
        var err error
        product, err = fetchFromDB(productID)
        return err
    })
    // Jika circuit OPEN → error menyebar → 500 error ke user
    return product, err
}

// ✓ Solusi: selalu ada fallback
func getProductSafe(productID string) (Product, error) {
    var product Product
    err := productBreaker.Call(func() error {
        var err error
        product, err = fetchFromDB(productID)
        return err
    })
    if errors.Is(err, ErrCircuitOpen) {
        // Fallback: cek cache, atau return data minimal
        cached, cacheErr := redis.Get(fmt.Sprintf("product:%s", productID))
        if cacheErr == nil {
            if err := json.Unmarshal([]byte(cached), &product); err == nil {
                return product, nil
            }
        }
        return Product{ID: productID, Status: "limited"}, nil
    }
    return product, err
}

// ✗ Anti-pattern 3: Threshold yang terlalu sensitif
sensitiveConfig := CircuitBreakerConfig{FailureThreshold: 1}  // 1 error → OPEN
// Network fluke biasa langsung membuat circuit terbuka

// ✗ Anti-pattern 4: Recovery timeout yang terlalu lama
slowConfig := CircuitBreakerConfig{RecoveryTimeout: time.Hour}  // 1 jam
// Dependency pulih dalam 30 detik tapi circuit tetap OPEN 1 jam
// Pengguna mengalami degraded service tidak perlu selama itu

// ✗ Anti-pattern 5: Circuit breaker tanpa monitoring
// Tidak ada yang tahu circuit terbuka sampai user mengeluh

// ✗ Anti-pattern 6: Include semua exception sebagai failure
allErrorsConfig := CircuitBreakerConfig{IsFailure: func(err error) bool { return true }}
// Bug dalam kode dihitung sebagai "dependency failure"
// ✓ Solusi: hanya network dan HTTP error yang dihitung sebagai failure
networkOnlyConfig := CircuitBreakerConfig{IsFailure: func(err error) bool {
    var netErr *net.OpError
    return errors.As(err, &netErr) || isHTTPError(err)
}}
#

Checklist Circuit Breaker #

DESAIN:
  □ Circuit breaker terpisah untuk setiap external dependency
  □ Threshold dikonfigurasi berdasarkan karakteristik dependency (bukan satu nilai untuk semua)
  □ Expected exceptions dikonfigurasi dengan benar (hanya network/timeout error)
  □ Fallback strategy terdefinisi untuk setiap circuit breaker

KONFIGURASI:
  □ failure_threshold: 5-10 kegagalan (atau 50-60% failure rate)
  □ failure_window: 60 detik (sesuaikan dengan traffic pattern)
  □ recovery_timeout: sesuai ekspektasi waktu recovery dependency
  □ success_threshold: 2-3 untuk confidence yang cukup di HALF-OPEN

FALLBACK:
  □ Ketika circuit OPEN, ada respons yang berguna (bukan hanya error)
  □ Fallback di-test secara teratur (chaos engineering)
  □ User mendapat pesan yang informatif tentang degraded service
  □ Data stale dari cache digunakan sebagai fallback bila memungkinkan

MONITORING:
  □ State setiap circuit breaker di-expose sebagai metric
  □ Alert dipasang untuk circuit yang OPEN (terutama critical path)
  □ Dashboard menampilkan health semua circuit breaker
  □ Failure rate per circuit di-track untuk early warning

BULKHEAD:
  □ Thread pool terpisah untuk dependency yang bisa lambat
  □ Connection pool per dependency dikonfigurasi dengan tepat
  □ Max concurrent calls per dependency dibatasi

Ringkasan #

  • Circuit breaker mencegah cascade failure — kegagalan satu dependency tidak menyebar ke seluruh sistem. Request gagal cepat (fast fail) alih-alih menunggu timeout dan memblokir thread.
  • Tiga state yang bekerja bersama — CLOSED (normal), OPEN (melindungi dependency), HALF-OPEN (probe recovery). Transisi otomatis berdasarkan threshold yang dikonfigurasi.
  • Fast fail adalah manfaat utama — circuit yang OPEN mengembalikan error dalam mikrodetik, bukan menunggu timeout 30 detik. Ini menjaga thread pool tetap tersedia untuk request lain.
  • Setiap dependency butuh circuit breaker sendiri — payment gateway, database, email service, search service — masing-masing punya karakteristik berbeda dan perlu threshold yang berbeda.
  • Fallback adalah bagian yang tidak bisa diabaikan — circuit breaker tanpa fallback hanya memindahkan masalah dari “lambat” ke “error”. Fallback yang baik memberikan degraded tapi masih berguna.
  • Threshold harus dikalibrasi — terlalu sensitif dan circuit terbuka untuk error kecil, terlalu longgar dan terlambat melindungi. Gunakan failure rate dengan minimum volume, bukan hanya jumlah absolut.
  • Bulkhead melengkapi circuit breaker — circuit breaker melindungi dari kualitas yang buruk, bulkhead melindungi dari kuantitas yang berlebihan. Keduanya dibutuhkan untuk resilience yang komprehensif.
  • Monitoring bukan opsional — tanpa metrics dan alert, circuit breaker yang terbuka tidak akan ketahuan sampai user mengeluh. State setiap circuit harus di-expose dan di-alert.
  • Library yang teruji lebih baik dari implementasi sendiri — pybreaker, gobreaker, opossum sudah menangani edge case yang tidak terpikirkan. Gunakan library untuk production, implementasi sendiri hanya untuk memahami konsep.
  • Expected exceptions harus dikonfigurasi dengan cermat — hanya network error, timeout, dan HTTP error yang menandakan masalah di dependency. Bug dalam kode sendiri (ValueError, TypeError) tidak boleh dihitung sebagai “dependency failure”.

← Sebelumnya: Broken Pipe   Berikutnya: Observability →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact