Knowledge Guide
HomeSystem DesignMicroservices Patterns

Retry Pattern An Example

The retry pattern converts a transient failure into an eventual success by re-invoking the exact same call after a pause that grows with each attempt and is randomized, but only while the error still looks transient and the attempt budget is not yet spent. Below is a framework-free Java utility that captures the whole mechanism: a bounded loop, exponential backoff, full jitter, error filtering, and correct interrupt handling.

import java.util.concurrent.Callable;
import java.util.concurrent.ThreadLocalRandom;

public final class RetryUtil {

    /** Absolute ceiling on any single wait, so exponential growth can't explode. */
    private static final long MAX_DELAY_MILLIS = 30_000;

    /**
     * Retry {@code task} up to {@code maxAttempts} times using exponential
     * backoff with optional full jitter.
     *
     * @param task            operation to run (returns T or throws)
     * @param maxAttempts     total tries: 1 initial + (maxAttempts-1) retries; must be >= 1
     * @param baseDelayMillis delay before the first retry
     * @param backoffFactor   multiplier per retry (2.0 = double); 1.0 = constant delay
     * @param useJitter       if true, wait a UNIFORM random value in [0, computedDelay]
     * @return the task's result on the first successful attempt
     * @throws Exception the last failure if every attempt fails
     */
    public static <T> T executeWithRetry(Callable<T> task, int maxAttempts,
                                         long baseDelayMillis, double backoffFactor,
                                         boolean useJitter) throws Exception {
        if (maxAttempts < 1) {
            throw new IllegalArgumentException("maxAttempts must be >= 1");
        }
        Exception lastException = null;

        for (int attempt = 1; attempt <= maxAttempts; attempt++) {
            try {
                return task.call();                 // success: return immediately
            } catch (Exception e) {
                lastException = e;
                if (!isRetryable(e) || attempt == maxAttempts) {
                    break;                          // non-transient, or out of tries
                }
            }

            // Exponential backoff, capped so it can't grow without bound.
            long computed = (long) (baseDelayMillis * Math.pow(backoffFactor, attempt - 1));
            long delay = Math.min(computed, MAX_DELAY_MILLIS);

            if (useJitter) {
                // FULL JITTER: sleep a uniform random value in [0, delay].
                delay = ThreadLocalRandom.current().nextLong(delay + 1);
            }

            try {
                Thread.sleep(delay);
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();  // preserve interrupt for callers
                break;                                // stop retrying promptly
            }
        }
        throw lastException;   // non-null here: we only reach this after a caught failure
    }

    /** Retry only transient faults; let programming/auth errors fail fast. */
    private static boolean isRetryable(Exception e) {
        // Real code matches timeouts, connection resets, HTTP 5xx --
        // NOT validation, auth, or 4xx client errors.
        return !(e instanceof IllegalArgumentException
                 || e instanceof SecurityException);
    }

    private RetryUtil() {}
}

The five parameters are the retry policy. maxAttempts is the total tries including the first (so 3 means 1 try + 2 retries); baseDelayMillis is the wait before the first retry; backoffFactor is the per-retry multiplier (2.0 doubles, 1.0 holds a constant delay); useJitter randomizes the wait to break up synchronized retry bursts. Three changes make this version robust rather than merely illustrative: a MAX_DELAY_MILLIS cap so exponential growth cannot balloon into a minutes-long sleep, an isRetryable filter so a bug or a 401 fails fast instead of being retried pointlessly, and ThreadLocalRandom instead of a shared Random under contention.

Why the naive version is wrong

A naive loop typically (a) catches Throwable and retries everything — including NullPointerException and auth failures that will never recover; (b) has no delay cap, so base=1s, factor=2, attempts=10 ends with a single 512-second sleep; (c) swallows InterruptedException silently, so shutdown or cancellation can't stop it; and (d) describes full-jitter waits as if they were the fixed exponential ceilings. With full jitter each wait is a uniform random draw in [0, ceiling], not the ceiling itself — the code and the narration must agree on that.

diagram
diagram

A concrete trace

Call it exactly as the guide's usage example does — executeWithRetry(() -> fetchData(url), 3, 1000, 2.0, true) — and suppose the first two calls throw TimeoutException and the third succeeds. Because useJitter=true, each wait is a fresh uniform draw between 0 and that attempt's exponential ceiling. One representative run:

attempttask.call()attempt == max (3)?ceiling = 1000·2^(n−1)full-jitter wait (this run)action
1throws TimeoutExceptionno1000·2⁰ = 1000 msrand[0,1000] → 640 mssleep 640, attempt→2
2throws TimeoutExceptionno1000·2¹ = 2000 msrand[0,2000] → 1310 mssleep 1310, attempt→3
3returns "OK"return "OK"

Total wait this run ≈ 640 + 1310 = 1950 ms. A different run of the same code might wait 120 ms then 1900 ms, or 900 ms then 40 ms — the totals differ every time.

diagram
diagram

This is the correction the page needed. With useJitter=true you cannot say "waits 1s then 2s" or "about 3 seconds of total wait" as if those were fixed — those figures are only the no-jitter ceilings. Full jitter shifts every wait to a uniform draw below its ceiling, so the honest statement is: the total wait is random, averages roughly half the no-jitter total (~1.5 s here), and is bounded above by ~3 s. That randomness is the entire point: it desynchronizes thousands of clients that all failed at the same instant, spreading their retries across the interval instead of firing them in one thundering herd.

Pitfalls

When to use it, and when not

Reach for retry when the failure is transient and independent of your input — a dropped connection, a momentary timeout, a leader election, an HTTP 503/429and the operation is idempotent (or made idempotent with a dedupe key). Concrete signal: the same call, unchanged, tends to succeed seconds later.

Avoid it when the operation has non-idempotent side effects with no dedupe, when the error is a permanent client error (retrying can't fix a 404), or when the downstream is already overloaded — there, more retries deepen the outage.

Trade-offs vs. alternatives

Takeaways


Re-authored and deepened for this guide. Jitter and backoff strategy follow the AWS Architecture Blog, "Exponential Backoff And Jitter" (Marc Brooker); pattern context and trade-offs draw on Michael Nygard, Release It! (2nd ed.) and Chris Richardson, microservices.io — Retry and Circuit Breaker patterns. Interrupt-handling guidance follows Brian Goetz et al., Java Concurrency in Practice.

🤖 Don't fully get this? Learn it with Claude

Stuck on Retry Pattern An Example? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Retry Pattern An Example** (System Design) and want to truly understand it. Explain Retry Pattern An Example from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Retry Pattern An Example** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Retry Pattern An Example** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Retry Pattern An Example** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes