Retry Strategy #

Di sistem terdistribusi, tidak semua kegagalan bermakna ada yang salah secara fundamental. Sebagian besar kegagalan bersifat transient — koneksi database yang sesaat penuh, API eksternal yang sedang melayani traffic spike, DNS lookup yang gagal satu kali tapi berhasil di percobaan berikutnya. Jika sistem langsung menyerah pada kegagalan pertama, banyak operasi yang seharusnya bisa berhasil akan menjadi error yang tidak perlu. Tapi jika sistem mencoba lagi tanpa strategi yang tepat — tanpa batasan, tanpa delay, tanpa mempertimbangkan jenis error — retry itu sendiri bisa menjadi sumber masalah: membombardir downstream yang sedang berjuang untuk pulih, atau mengeksekusi operasi non-idempotent dua kali dengan konsekuensi yang tidak diinginkan. Artikel ini membahas retry strategy secara menyeluruh: enam jenis retry dengan karakteristik dan trade-off masing-masing, implementasi konkret di Go dan Dart, klasifikasi error yang boleh dan tidak boleh di-retry, hingga integrasi dengan circuit breaker dan DLQ.

Apa Itu Retry Strategy? #

Retry Strategy adalah pendekatan sistematis untuk mencoba kembali operasi yang gagal, dengan aturan dan batasan yang jelas. Bukan sekadar loop yang mengulangi operasi sampai berhasil, melainkan mekanisme yang sadar tentang kapan retry masuk akal, berapa lama jeda yang diperlukan, dan kapan harus menyerah.

// ANTI-PATTERN: retry tanpa strategi — berbahaya
func callServiceDangerous() error {
    for {                          // ← tidak ada batas
        err := callService()
        if err == nil { return nil }
        // tidak ada delay — DDoS ke diri sendiri
        // tidak ada klasifikasi error — retry validation error yang tidak akan berubah
    }
}

// BENAR: retry dengan konfigurasi eksplisit
func callServiceSafe(ctx context.Context) error {
    return RetryWithBackoff(ctx, RetryConfig{
        MaxAttempts:   5,
        BaseDelay:     100 * time.Millisecond,
        MaxDelay:      30 * time.Second,
        Jitter:        true,
        IsRetryable:   isTransientError,
    }, func() error {
        return callService()
    })
}
// Error 4xx → fail fast tanpa retry
// Error 5xx/timeout → retry dengan increasing delay
// Setelah max attempts → return final error
flowchart TD
    A[Operasi Gagal] --> B{IsRetryable?}
    B -- Tidak\n4xx, validation --> C["Fail Fast\nReturn Error"]
    B -- Ya\n5xx, timeout, 429 --> D{"Attempt <=\nMaxAttempts?"}
    D -- Tidak --> E["Exhausted\nKirim ke DLQ\natau Return Error"]
    D -- Ya --> F["Hitung Delay\nexponential + jitter"]
    F --> G[Tunggu Delay]
    G --> H[Retry Operasi]
    H --> I{Berhasil?}
    I -- Ya --> J[Sukses ✓]
    I -- Tidak --> B

Mengapa Retry Strategy Penting #

Ada tiga alasan fundamental yang membuat retry bukan opsional di sistem modern.

Kegagalan transient adalah norma, bukan pengecualian. Network timeout, DNS hiccup, service overload sementara, cold start di serverless, lock contention yang segera terlepas — semua ini adalah kegagalan yang sembuh dengan sendirinya dalam hitungan milidetik hingga detik. Tanpa retry, setiap kegagalan transient menjadi visible error yang harus ditangani caller atau, lebih buruk, menjadi noise yang membanjiri alert.

Retry adalah fondasi resilience pattern. Circuit breaker, bulkhead, dan timeout semuanya berasumsi bahwa ada mekanisme retry di baliknya. Setelah circuit tertutup kembali usai periode cooldown, sistem harus bisa retry operasi yang sebelumnya gagal. Tanpa retry yang benar, pattern-pattern ini kurang efektif.

Reliability tanpa intervensi manual. Sistem yang tidak bisa self-heal dari kegagalan transient membutuhkan on-call engineer untuk merespons setiap incident kecil. Retry yang dirancang baik mengurangi incident rate dan membuat sistem lebih mandiri.


Enam Jenis Retry Strategy #

1. Immediate Retry #

Retry langsung tanpa jeda. Hanya cocok untuk operasi in-memory atau kasus di mana kegagalan benar-benar bersifat satu-kali dan sembuh dalam mikrodetik.

// ANTI-PATTERN: immediate retry untuk network call
for attempt := 0; attempt < maxAttempts; attempt++ {
    err := callExternalService()
    if err == nil { return nil }
    // ← tidak ada delay
    // → saat downstream down 100ms, semua retry langsung flood bersamaan
}

// BENAR: immediate retry hanya untuk operasi in-memory yang benar-benar microsecond
for attempt := 0; attempt < 3; attempt++ {
    if acquired := mutex.TryLock(); acquired {
        return nil
    }
    // mutex contention sembuh dalam nanosecond, immediate retry aman di sini
    runtime.Gosched() // yield CPU, coba lagi segera
}
Jangan gunakan immediate retry untuk network call atau operasi I/O. Jika downstream down selama 100ms, semua instance yang mengalami error bersamaan akan menghantam downstream secara serentak saat mereka retry — memperparah kondisi yang sudah overload.

2. Fixed Delay Retry #

Retry dengan jeda waktu yang sama setiap kali. Lebih aman dari immediate retry tapi masih berpotensi thundering herd.

// Fixed delay — sederhana, cocok untuk sistem sederhana dengan satu instance
func retryFixed(ctx context.Context, maxAttempts int, delay time.Duration,
    fn func() error) error {

    var lastErr error
    for attempt := 1; attempt <= maxAttempts; attempt++ {
        lastErr = fn()
        if lastErr == nil {
            return nil
        }
        if attempt < maxAttempts {
            select {
            case <-time.After(delay):
            case <-ctx.Done():
                return ctx.Err()
            }
        }
    }
    return fmt.Errorf("all %d attempts failed: %w", maxAttempts, lastErr)
}

// ANTI-PATTERN: ratusan instance retry dengan delay persis sama
// → thundering herd: semua 200 instance retry setelah tepat 1 detik
// → downstream menerima spike 200 request sekaligus, bukan spread merata

3. Exponential Backoff #

Delay berlipat ganda setiap kali retry gagal, memberi downstream waktu yang semakin panjang untuk pulih.

// Delay: 100ms → 200ms → 400ms → 800ms → 1600ms (capped)
func calculateBackoff(attempt int, base, max time.Duration) time.Duration {
    delay := base * time.Duration(1<<uint(attempt-1)) // base × 2^(attempt-1)
    if delay > max {
        delay = max
    }
    return delay
}
flowchart LR
    A["Attempt 1\nFail"] -->|"100ms"| B["Attempt 2\nFail"]
    B -->|"200ms"| C["Attempt 3\nFail"]
    C -->|"400ms"| D["Attempt 4\nFail"]
    D -->|"800ms"| E["Attempt 5\nFail/Success"]
    style A fill:#ffcccc
    style B fill:#ffcccc
    style C fill:#ffcccc
    style D fill:#ffcccc

Jauh lebih baik dari fixed delay, tapi masih thundering herd jika banyak instance mulai retry secara bersamaan karena semua menggunakan delay yang identik.

4. Exponential Backoff + Jitter — Best Practice #

Menambahkan randomisasi pada delay untuk menyebar timing retry secara alami. Ini adalah strategi yang paling direkomendasikan untuk hampir semua production use case.

// Implementasi lengkap: exponential backoff + full jitter
func RetryWithBackoff(ctx context.Context, cfg RetryConfig, fn func() error) error {
    var lastErr error

    for attempt := 1; attempt <= cfg.MaxAttempts; attempt++ {
        lastErr = fn()

        if lastErr == nil {
            if attempt > 1 {
                log.Infof("retry succeeded on attempt %d", attempt)
            }
            return nil
        }

        // Fail fast untuk error yang tidak layak di-retry
        if !cfg.IsRetryable(lastErr) {
            return fmt.Errorf("non-retryable error: %w", lastErr)
        }

        if attempt == cfg.MaxAttempts {
            break // jangan tunggu setelah attempt terakhir
        }

        // Hitung base delay: min(base × 2^(attempt-1), maxDelay)
        baseDelay := cfg.BaseDelay * time.Duration(1<<uint(attempt-1))
        if baseDelay > cfg.MaxDelay {
            baseDelay = cfg.MaxDelay
        }

        // Full jitter: random antara 0 dan baseDelay
        // Lebih efektif daripada ±25% dalam menghindari thundering herd
        jitter := time.Duration(rand.Int63n(int64(baseDelay)))

        log.Warnf("attempt %d/%d failed: %v — retrying in %v",
            attempt, cfg.MaxAttempts, lastErr, jitter)

        select {
        case <-time.After(jitter):
        case <-ctx.Done():
            return ctx.Err()
        }
    }

    return fmt.Errorf("exhausted %d attempts: %w", cfg.MaxAttempts, lastErr)
}

type RetryConfig struct {
    MaxAttempts int
    BaseDelay   time.Duration
    MaxDelay    time.Duration
    IsRetryable func(error) bool
}

Perbandingan delay dengan dan tanpa jitter:

AttemptTanpa JitterDengan Full Jitter
1 → 2100ms (semua instance)0–100ms (random per instance)
2 → 3200ms (semua instance)0–200ms (random per instance)
3 → 4400ms (semua instance)0–400ms (random per instance)
4 → 5800ms (semua instance)0–800ms (random per instance)

Dengan jitter, 200 instance yang gagal bersamaan tidak akan semua retry di waktu yang sama — mereka tersebar secara alami sepanjang window delay.

5. Retry dengan Deadline / Timeout Budget #

Alih-alih membatasi jumlah attempt, batasi total waktu yang boleh dipakai. Sangat cocok untuk sistem latency-sensitive di mana user experience bergantung pada respons cepat.

// Retry sampai deadline — bukan sampai max attempt
func RetryUntilDeadline(ctx context.Context, timeout time.Duration,
    fn func() error) error {

    deadline := time.Now().Add(timeout)
    ctx, cancel := context.WithDeadline(ctx, deadline)
    defer cancel()

    attempt := 0
    baseDelay := 50 * time.Millisecond

    for {
        attempt++
        err := fn()
        if err == nil {
            return nil
        }

        remaining := time.Until(deadline)
        if remaining <= 0 {
            return fmt.Errorf("deadline exceeded after %d attempts: %w", attempt, err)
        }

        nextDelay := baseDelay * time.Duration(1<<uint(attempt-1))
        // Jangan retry jika delay berikutnya lebih dari setengah remaining time
        if nextDelay > remaining/2 {
            return fmt.Errorf("insufficient time budget for retry: %w", err)
        }

        jitter := time.Duration(rand.Int63n(int64(nextDelay)))
        select {
        case <-time.After(jitter):
        case <-ctx.Done():
            return ctx.Err()
        }
    }
}

6. Retry dengan Circuit Breaker #

Circuit breaker dan retry bekerja bersama: retry menangani kegagalan transient, circuit breaker mencegah retry yang sia-sia saat downstream sudah jelas tidak bisa diakses.

sequenceDiagram
    participant C as Caller
    participant CB as Circuit Breaker
    participant D as Downstream

    C->>CB: Attempt 1
    CB->>D: Forward (CLOSED)
    D-->>CB: Error
    CB-->>C: Error → retry

    C->>CB: Attempt 2
    CB->>D: Forward (CLOSED)
    D-->>CB: Error
    CB-->>C: Error → retry

    C->>CB: Attempt 3
    CB->>D: Forward (CLOSED)
    D-->>CB: Error
    Note over CB: failure rate > threshold\nCircuit OPEN

    C->>CB: Attempt 4
    CB-->>C: Fail fast — circuit OPEN\ntanpa panggil downstream

    Note over CB: Setelah cooldown: HALF-OPEN
    C->>CB: Attempt 5
    CB->>D: Satu probe request
    D-->>CB: Success
    Note over CB: Circuit CLOSED kembali
    CB-->>C: Success ✓

Klasifikasi Error: Boleh dan Tidak Boleh di-Retry #

Ini adalah keputusan paling kritis dalam merancang retry strategy. Retry error yang salah bisa menyembunyikan bug atau memperparah situasi.

// Fungsi klasifikasi error yang eksplisit
func isRetryableError(err error) bool {
    if err == nil {
        return false
    }

    // Context cancelled atau deadline exceeded — jangan retry
    if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
        return false
    }

    var httpErr *HTTPError
    if errors.As(err, &httpErr) {
        switch httpErr.StatusCode {
        // ✓ BOLEH retry — error transient
        case 429: return true // Too Many Requests — tunggu dan coba lagi
        case 500: return true // Internal Server Error — mungkin transient
        case 502: return true // Bad Gateway — upstream sedang restart
        case 503: return true // Service Unavailable — downstream overload
        case 504: return true // Gateway Timeout

        // ✗ JANGAN retry — error deterministic
        case 400: return false // Bad Request — request salah, tidak akan berubah
        case 401: return false // Unauthorized — perlu refresh token dulu
        case 403: return false // Forbidden — tidak punya akses
        case 404: return false // Not Found — resource tidak ada
        case 409: return false // Conflict — butuh logic khusus, bukan sekedar retry
        case 422: return false // Unprocessable Entity — data tidak valid
        }
    }

    // Network errors transient — biasanya layak di-retry
    var netErr *net.OpError
    if errors.As(err, &netErr) {
        return true
    }

    return false // default: jangan retry jika tidak yakin
}

Ringkasan klasifikasi:

HTTP StatusRetryable?Alasan
400 Bad Request❌ TidakRequest salah — tidak akan berubah kalau diulang
401 Unauthorized❌ TidakPerlu refresh token terlebih dahulu
403 Forbidden❌ TidakTidak punya akses, retry tidak akan membantu
404 Not Found❌ TidakResource tidak ada
409 Conflict❌ TidakButuh logic resolusi, bukan sekedar retry
422 Unprocessable❌ TidakData tidak valid
429 Too Many Requests✅ YaRate limited — tunggu dan coba lagi
500 Server Error✅ YaMungkin transient
502 Bad Gateway✅ YaUpstream sedang restart
503 Unavailable✅ YaDownstream overload sementara
504 Gateway Timeout✅ YaTimeout — mungkin berhasil jika retry
Network timeout✅ YaTransient connectivity issue
context.Canceled❌ TidakUser/caller sudah cancel — buang

Rule of thumb: jika menjalankan operasi yang sama dengan input yang sama tidak mungkin menghasilkan hasil berbeda, jangan retry. Validation error dan authorization error hampir tidak pernah layak di-retry tanpa mengubah sesuatu terlebih dahulu.


Retry dan Idempotency — Hubungan yang Tidak Bisa Dipisahkan #

Retry tanpa mempertimbangkan idempotency adalah bug laten. Setiap operasi yang di-retry harus aman untuk dieksekusi lebih dari sekali.

// ANTI-PATTERN: retry operasi yang tidak idempotent
func processPayment(userID string, amount int64) error {
    return RetryWithBackoff(ctx, defaultConfig, func() error {
        // Jika request berhasil tapi response timeout sebelum sampai,
        // retry akan memproses PAYMENT KEDUA — double charge!
        return paymentGateway.Charge(userID, amount)
    })
}

// BENAR: idempotency key memastikan retry selalu aman
func processPayment(idempotencyKey, userID string, amount int64) error {
    return RetryWithBackoff(ctx, defaultConfig, func() error {
        return paymentGateway.Charge(ChargeRequest{
            IdempotencyKey: idempotencyKey, // ← gateway dedup berdasarkan key ini
            UserID:         userID,
            Amount:         amount,
        })
        // Jika request sebelumnya sudah berhasil, gateway return hasil yang sama
        // tanpa memproses ulang — retry 100% aman
    })
}
Aturan wajib: sebelum menambahkan retry ke sebuah operasi, pastikan dulu operasi tersebut idempotent — atau buat ia idempotent dengan idempotency key. Retry tanpa idempotency pada operasi finansial atau state-mutation adalah resep double processing yang bisa sangat mahal biayanya.

Retry di Berbagai Layer Sistem #

Salah satu kesalahan yang sering terjadi adalah menambahkan retry di setiap layer tanpa koordinasi — menghasilkan retry amplification yang memperparah beban pada downstream.

flowchart TD
    subgraph Amplification["❌ Retry Amplification — Setiap Layer Retry Sendiri"]
        C1["Client\n3x retry"] --> G1["API Gateway\n3x retry"]
        G1 --> S1["Service A\n3x retry"]
        S1 --> D1["Downstream\n3×3×3 = 27 calls\nuntuk 1 request user!"]
    end
    subgraph Best["✅ Best Practice — Retry di Satu Layer Tepat"]
        C2["Client\nfail fast"] --> G2["API Gateway\n3x retry ← di sini saja"]
        G2 --> S2["Service A\nfail fast"]
        S2 --> D2["Downstream\nmaks 3 calls saja"]
    end

Panduan retry per layer:

LayerRetry Cocok UntukCatatan
HTTP ClientTimeout, 5xx dari upstreamWajib ada idempotency key
Message ConsumerTransient processing errorKombinasikan dengan DLQ
Background WorkerExternal API call yang flakyKombinasikan dengan job state persistence
Database ClientDeadlock, connection error transientSudah built-in di banyak driver
gRPC ClientStatus UNAVAILABLE, DEADLINE_EXCEEDEDGunakan retry policy di service config

Implementasi di Dart/Flutter (Dio Interceptor) #

// Retry transport untuk http.Client — berlaku untuk semua HTTP request
type RetryTransport struct {
    base       http.RoundTripper
    maxRetries int
    baseDelay  time.Duration
}

func (t *RetryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    retryCount := 0
    for {
        resp, err := t.base.RoundTrip(req)

        if !isRetryable(err, resp) || retryCount >= t.maxRetries {
            return resp, err
        }

        retryCount++
        // Exponential backoff dengan full jitter
        maxDelayMs := int64(t.baseDelay.Milliseconds()) * int64(1<<uint(retryCount))
        jitterMs := rand.Int63n(maxDelayMs)

        log.Printf("[RETRY] attempt %d/%d dalam %dms untuk %s",
            retryCount, t.maxRetries, jitterMs, req.URL.Path)

        time.Sleep(time.Duration(jitterMs) * time.Millisecond)
    }
}

func isRetryable(err error, resp *http.Response) bool {
    // Network error transient
    if err != nil {
        return true
    }
    // HTTP status yang retryable
    return resp != nil && (resp.StatusCode == 429 || resp.StatusCode == 500 ||
        resp.StatusCode == 502 || resp.StatusCode == 503 || resp.StatusCode == 504)
}

// Setup di main
func setupClient() *http.Client {
    return &http.Client{
        Transport: &RetryTransport{maxRetries: 3, baseDelay: 200 * time.Millisecond},
    }
}

Observability untuk Retry #

Retry yang tidak ter-observe adalah retry yang tidak bisa di-tune. Tanpa metrics, kamu tidak tahu apakah retry configuration sudah optimal atau justru menyembunyikan masalah yang lebih dalam.

// Metrics wajib untuk setiap retry mechanism
func RetryWithMetrics(ctx context.Context, operationName string,
    cfg RetryConfig, fn func() error) error {

    startTime := time.Now()
    totalAttempts := 0

    err := RetryWithBackoff(ctx, cfg, func() error {
        totalAttempts++
        attemptErr := fn()

        if attemptErr != nil && totalAttempts > 1 {
            metrics.Counter("retry.attempt",
                "operation", operationName,
                "attempt", strconv.Itoa(totalAttempts),
            ).Inc()
        }
        return attemptErr
    })

    duration := time.Since(startTime)

    if err != nil {
        // Semua retry habis dan masih gagal
        metrics.Counter("retry.exhausted", "operation", operationName).Inc()
        metrics.Histogram("retry.duration_on_failure",
            "operation", operationName,
        ).Record(duration.Seconds())
    } else if totalAttempts > 1 {
        // Berhasil setelah retry — track berapa attempt
        metrics.Counter("retry.recovered",
            "operation", operationName,
            "attempts", strconv.Itoa(totalAttempts),
        ).Inc()
    }

    return err
}

Metrics yang wajib dipantau:

MetrikArtinyaAlert Jika
retry.attempt countBerapa kali retry dilakukanNaik terus → downstream mulai bermasalah
retry.exhausted countRetry habis, masih gagal> 0 → butuh investigasi
retry.recovered countBerhasil setelah retryPerlu untuk menghitung recovery rate
retry.duration_on_failureTotal waktu termasuk semua retryTerlalu tinggi → turunkan max attempts

Anti-Pattern yang Harus Dihindari #

// ✗ Infinite retry — sistem stuck, resource leak
for {
    err := callService()
    if err == nil { break }
    time.Sleep(100 * time.Millisecond)
}
// ✓ Selalu ada MaxAttempts atau total timeout budget

// ✗ Retry semua error tanpa klasifikasi
RetryWithBackoff(ctx, cfg, func() error {
    return validateUserInput(req) // 400 tidak akan berubah kalau di-retry!
})
// ✓ Definisikan IsRetryable yang eksplisit

// ✗ Retry berlapis tanpa koordinasi — amplification 3×3×3 = 27 calls
// ✓ Tentukan satu layer yang bertanggung jawab, layer lain fail-fast

// ✗ Tidak ada delay — immediate retry untuk network call
for attempt := 0; attempt < 5; attempt++ {
    callExternalService() // flood ke downstream
}
// ✓ Exponential backoff + jitter

// ✗ Retry operasi non-idempotent tanpa idempotency key
RetryWithBackoff(ctx, cfg, func() error {
    return chargeCard(userID, amount) // bisa double charge!
})
// ✓ Pastikan idempotency sebelum menambahkan retry

// ✗ Sembunyikan semua error setelah retry habis
if err := RetryWithBackoff(...); err != nil {
    log.Error(err)
    return nil // ← caller tidak tahu ada masalah!
}
// ✓ Propagate error, biarkan caller memutuskan penanganan selanjutnya

// ✗ Retry setelah context cancelled
for attempt := 1; attempt <= maxAttempts; attempt++ {
    fn()
    time.Sleep(delay) // tidak cek ctx.Done()!
}
// ✓ Selalu cek ctx.Done() di select sebelum tunggu delay

Checklist Implementasi Retry Strategy #

KONFIGURASI:
  □ MaxAttempts atau total timeout budget sudah ditetapkan (tidak infinite)
  □ BaseDelay, MaxDelay sudah disesuaikan dengan SLA downstream
  □ Full jitter diaktifkan untuk mencegah thundering herd

KLASIFIKASI ERROR:
  □ IsRetryable function sudah didefinisikan secara eksplisit
  □ 4xx errors (kecuali 429) dikecualikan dari retry
  □ context.Canceled dan context.DeadlineExceeded dikecualikan dari retry

IDEMPOTENCY:
  □ Semua operasi yang di-retry sudah dipastikan idempotent
  □ Idempotency key digunakan untuk operasi non-idempotent

LAYER DESIGN:
  □ Tidak ada retry bertumpuk di multiple layer untuk operasi yang sama
  □ Layer yang bertanggung jawab retry sudah ditentukan

OBSERVABILITY:
  □ retry.attempt, retry.exhausted, retry.recovered di-track sebagai metrics
  □ Alert terpasang jika retry.exhausted > threshold
  □ Log mencantumkan attempt number, delay, dan error message

TESTING:
  □ Test untuk happy path: berhasil di attempt pertama
  □ Test untuk retry path: gagal N kali, berhasil di attempt terakhir
  □ Test untuk exhausted: gagal semua attempt, DLQ atau final error
  □ Test untuk non-retryable: langsung fail fast tanpa retry

Ringkasan #

  • Retry Strategy adalah pendekatan sistematis — bukan loop biasa; memiliki aturan jelas tentang kapan, berapa kali, berapa lama, dan untuk error apa retry dilakukan.
  • Enam jenis retry: immediate (hindari untuk network), fixed delay (thundering herd risk), exponential backoff (lebih baik), exponential backoff + jitter (best practice), deadline-based (latency-sensitive), kombinasi circuit breaker (mencegah retry yang sia-sia).
  • Exponential backoff + full jitter adalah default terbaik — memberi downstream waktu pulih sekaligus menyebar timing retry untuk menghindari thundering herd.
  • Klasifikasi error adalah kritis: 4xx (kecuali 429) hampir tidak pernah layak di-retry; 5xx dan timeout biasanya layak; context.Canceled tidak boleh di-retry.
  • Idempotency adalah prasyarat mutlak — sebelum menambahkan retry ke operasi apapun, pastikan operasi tersebut aman dijalankan lebih dari sekali; gunakan idempotency key jika perlu.
  • Retry amplification terjadi saat setiap layer melakukan retry independen — 3×3×3 = 27 call untuk 1 request user; tetapkan satu layer yang bertanggung jawab.
  • Context cancellation harus dihormati — selalu cek ctx.Done() di antara retry; jangan teruskan setelah context cancelled.
  • Observability wajib — track retry count, recovery rate, dan exhausted count; tanpa metrics tidak bisa tahu apakah konfigurasi sudah optimal.
  • Infinite retry adalah anti-pattern — selalu ada max attempts atau total timeout budget untuk mencegah resource leak dan sistem stuck.

← Sebelumnya: Aspect Oriented Programming   Berikutnya: Backoff Strategy →

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