Series MapLesson 35 / 112
Focus mode active/Press Alt+Shift+R to toggle/Esc to exit
Build CoreOrdered learning track

Retry, Circuit Breaker, and Bulkhead

Retry Retry Budget Circuit Breaker and Bulkhead

Resilience pattern untuk dependency failure: retry, retry budget, circuit breaker, bulkhead, fallback boundary, dan integrasi aman di service JAX-RS production

8 min read1538 words
PrevNext
Lesson 35112 lesson track22–61 Build Core
#resilience#retry#retry-budget#circuit-breaker+4 more

Part 035 — Retry, Retry Budget, Circuit Breaker, and Bulkhead

Fokus part ini: memahami retry, retry budget, circuit breaker, dan bulkhead sebagai mekanisme pengendalian failure. Tujuannya bukan membuat request “lebih sering dicoba”, tetapi mencegah satu dependency failure berubah menjadi cascading failure di sistem enterprise.

Retry sering terlihat seperti solusi sederhana:

Call failed? Try again.

Di production, retry yang salah bisa menjadi amplifier:

small downstream latency -> retry starts -> traffic doubles/triples
                       -> downstream overloads more
                       -> timeout increases
                       -> more retry
                       -> retry storm

Senior engineer tidak bertanya:

Should we retry?

Tetapi:

Is this operation safe to retry, within what budget, under which failure class,
and how do we stop retry from harming the system?

1. Core Mental Model

Resilience pattern bekerja pada boundary antar komponen.

flowchart LR A[JAX-RS Resource] --> B[Application Service] B --> C[HTTP Client] B --> D[Database] B --> E[Kafka Producer] B --> F[Redis] C --> G[Downstream Service]

Setiap boundary punya kemungkinan:

- fast success
- fast failure
- slow success
- slow failure
- timeout
- partial side effect
- duplicate execution
- overload

Retry, circuit breaker, dan bulkhead bukan pengganti correctness.

Mereka adalah guardrail untuk:

- membatasi blast radius
- menjaga resource tetap bounded
- memberi waktu dependency pulih
- melindungi service dari antrian kerja yang tidak lagi bernilai
- menjaga sebagian sistem tetap berjalan saat dependency tertentu rusak

2. Failure Classes Before Retry

Tidak semua failure layak retry.

Failure classExampleRetry?Reason
transient network failureconnection reset, temporary DNS glitchmaybebisa pulih cepat
timeoutdownstream slowmaybe, but dangerousretry bisa memperparah overload
429 rate limiteddependency menolak karena quotaonly if Retry-After respectedharus patuh budget
503 unavailabledependency overloaded/downlimited retryjangan hammer dependency
400 bad requestinvalid inputnorequest tetap salah
401/403auth/authz failurenoretry tidak mengubah permission
404resource absentusually nokecuali eventual consistency jelas
409 conflictoptimistic lock/idempotency conflictdependsperlu domain-specific decision
DB deadlocktransaction deadlockmayberetry transaction bisa valid
unique constraint violationduplicate keyusually nosering signal idempotency/conflict
Kafka retriable exceptionmetadata/leader changeyes, client-levelKafka client sudah punya retry semantics

Decision rule:

Retry only when:
1. failure is likely transient
2. operation is safe to repeat
3. retry stays inside request deadline
4. retry is bounded by budget
5. retry has backoff/jitter
6. system has observability for retry impact

3. Retry Safety and Idempotency

Retry aman hanya jika side effect bisa dikontrol.

Unsafe example:

POST /orders
- creates order
- charges customer
- publishes event
- first response times out
- client retries
- second call creates duplicate order or duplicate charge

Safer design:

POST /orders
Idempotency-Key: abc-123

Server stores:
- request identity
- semantic operation key
- processing state
- final response or conflict result

Retry safety depends on:

- HTTP method semantics
- idempotency key
- database uniqueness guard
- deduplication table
- outbox/inbox pattern
- downstream idempotency support
- compensating transaction availability

For Java/JAX-RS service, retry safety is usually enforced below the resource method:

Resource method
  -> application service
     -> idempotency guard
        -> transaction
           -> repository / downstream call / event outbox

Bad placement:

@POST
@Path("/orders")
public Response createOrder(CreateOrderRequest request) {
    // Anti-pattern: blind retry around entire business operation
    for (int i = 0; i < 3; i++) {
        try {
            return Response.ok(orderService.create(request)).build();
        } catch (Exception e) {
            // retries everything, including partial side effects
        }
    }
    throw new RuntimeException("failed");
}

Better reasoning:

- Retry small, well-understood operations.
- Do not blindly retry large business workflows.
- Prefer idempotent command boundary.
- Persist state before publishing events.
- Do not hide duplicate execution from domain model.

4. Retry Budget

Retry budget answers:

How much extra traffic are we willing to generate because of failure?

Without budget:

1000 original requests/sec
3 retry attempts
= up to 3000 additional attempts/sec
= 4000 total calls/sec under failure

Retry budget can be expressed as:

- max attempts per call
- max total retry duration
- max retry traffic ratio
- max retries per endpoint/dependency/tenant
- max retries within request deadline

Simple budget model:

Allowed retry traffic <= 10% of successful baseline traffic

Request-level budget:

request deadline: 2 seconds
attempt 1 timeout: 500ms
backoff: 100ms
attempt 2 timeout: 500ms
backoff: 200ms
attempt 3 timeout: 300ms
remaining budget reserved for response/error mapping

Bad retry config:

attempts = 5
read timeout = 5s
backoff = 1s
client timeout = 10s

This cannot possibly fit the client deadline.

Good retry design starts from deadline:

request deadline -> dependency budget -> attempt timeout -> retry count -> backoff

5. Backoff and Jitter

Retry without delay often creates synchronized load.

Bad:

attempt 1 fails at t=0
all clients retry immediately at t=0
all clients retry again at t=0

Better:

attempt 1 fails
wait randomized delay
retry while still inside deadline

Backoff strategies:

StrategyBehaviorUse case
fixed delaysame wait every timesimple low-volume internal retry
exponential backoffdelay grows per attemptoverloaded dependency recovery
jitterrandomized delayavoids synchronized retry storm
server-directed delayobeys Retry-Afterrate limit / maintenance window

Conceptual policy:

maxAttempts = 3
attemptTimeout = bounded by remaining deadline
backoff = exponential + jitter
retryOn = transient exceptions only
abortOn = validation/auth/domain errors

6. Circuit Breaker Mental Model

Circuit breaker prevents repeated calls to a dependency that is already failing.

stateDiagram-v2 [*] --> CLOSED CLOSED --> OPEN: failure rate / slow call rate exceeds threshold OPEN --> HALF_OPEN: wait duration elapsed HALF_OPEN --> CLOSED: trial calls succeed HALF_OPEN --> OPEN: trial calls fail

States:

StateMeaningBehavior
closeddependency considered healthycalls allowed
opendependency considered unhealthycalls fail fast
half-opentesting recoverylimited trial calls

Circuit breaker protects:

- caller threads
- connection pools
- dependency itself
- upstream latency
- user experience through fast failure/fallback

Circuit breaker does not fix:

- wrong timeout
- unsafe retry
- bad idempotency
- database lock contention
- poor capacity planning

7. Circuit Breaker Signals

A breaker may open based on:

- failure rate
- slow call rate
- number of calls in sliding window
- timeout rate
- specific exception classes
- HTTP status classes

Important distinction:

HTTP 500 from dependency may count as failure.
HTTP 404 may or may not count as failure.
HTTP 409 may be domain conflict, not dependency failure.
HTTP 429 should usually influence rate/retry policy.

Bad breaker configuration:

- every exception opens breaker
- validation error counted as dependency failure
- window too small, breaker flaps
- half-open allows too many calls
- open duration too long, service never recovers quickly

Senior-level question:

Which errors represent dependency health, and which errors represent normal business outcomes?

8. Bulkhead Mental Model

Bulkhead isolates resource pools so one failing dependency or workload cannot consume everything.

Ship analogy:

If one compartment floods, the entire ship should not sink.

Service analogy:

If pricing service is slow, catalog lookup should not consume all request threads.
If tenant A creates heavy traffic, tenant B should not starve.
If reporting query is slow, order submission should remain available.

Common bulkhead dimensions:

- by dependency
- by endpoint type
- by tenant
- by priority
- by workload class: read/write/reporting/job
- by execution pool
- by connection pool

Bulkhead forms:

Bulkhead typeDescriptionRisk
semaphore bulkheadlimits concurrent callscaller thread still waits/blocks
thread-pool bulkheadisolates execution threads/queuecontext propagation and queue tuning needed
connection-pool bulkheadseparate pool per dependencymore config and capacity planning
tenant quotaisolates tenantsrequires accurate tenant resolution
endpoint quotaprotects critical endpointsmay reject non-critical traffic

9. Retry + Circuit Breaker + Bulkhead Interaction

These patterns interact. Wrong order creates bad behavior.

Common conceptual chain:

Caller
  -> deadline check
  -> bulkhead
  -> circuit breaker
  -> retry policy
  -> timeout-bound dependency call

But the exact order depends on library and use case.

What matters:

- retry must not bypass circuit breaker
- retry must not exceed bulkhead capacity
- timeout must bound each attempt
- circuit breaker must see meaningful failures
- fallback must not hide data corruption

Example failure:

bulkhead allows 100 concurrent calls
retry attempts = 3
real dependency concurrency becomes up to 300 attempts over time

Better design:

bulkhead capacity and retry budget are designed together

10. Resilience4j or Equivalent Library

Many Java services use a resilience library such as Resilience4j, a platform wrapper, service mesh policy, API gateway policy, or internal framework.

Do not assume the library. Verify it.

Possible places resilience may be applied:

- Java code using Resilience4j decorators
- annotations/aspects around service methods
- HTTP client wrapper
- generated client layer
- service mesh timeout/retry policy
- API gateway retry/rate limit policy
- Kubernetes ingress/controller annotations
- platform SDK used internally

Conceptual Java shape:

public final class PricingClient {
    private final RemotePricingApi api;
    private final Retry retry;
    private final CircuitBreaker circuitBreaker;
    private final Bulkhead bulkhead;

    public PriceResult quotePrice(PricingRequest request, Deadline deadline) {
        Supplier<PriceResult> call = () -> api.quotePrice(request, deadline.remaining());

        Supplier<PriceResult> guarded = Decorators.ofSupplier(call)
            .withBulkhead(bulkhead)
            .withCircuitBreaker(circuitBreaker)
            .withRetry(retry)
            .decorate();

        return guarded.get();
    }
}

The exact API may differ. The important design is:

- dependency-specific policy
- bounded retry
- breaker by dependency/operation
- metrics emitted
- fallback explicit
- no blanket retry around domain workflow

11. Where to Apply Resilience in JAX-RS Service

Avoid placing all resilience at resource method level.

Resource method should usually handle:

- request parsing
- auth/security context availability
- validation trigger
- application service call
- response mapping

Resilience usually belongs at integration boundary:

Application service
  -> PricingClient with timeout/retry/breaker/bulkhead
  -> CatalogClient with own policy
  -> OrderRepository with DB timeout/transaction policy
  -> EventPublisher with producer policy

Why boundary-specific?

- pricing dependency may tolerate fallback
- order database write may not tolerate retry without transaction design
- catalog read may be cacheable
- event publish may require outbox instead of direct retry

Anti-pattern:

One global retry policy for all outbound calls.

Better:

Policy per dependency + operation + side-effect profile.

12. Fallback Boundary

Fallback is not “return anything so the endpoint succeeds.”

Fallback must preserve truthfulness.

Possible fallback types:

FallbackExampleRisk
cached readreturn last known catalogstale data
default valueuse default feature configwrong business behavior
partial responseomit non-critical enrichmentclient must understand partiality
queue for lateraccept command asynchronouslyrequires state tracking
fail fastreturn explicit erroruser sees failure but system protected

Unsafe fallback:

pricing service unavailable -> return price = 0

Safer fallback:

pricing service unavailable -> return 503 with retryable error code
or return quote status = PRICING_PENDING if workflow supports async completion

Senior-level rule:

A fallback must be a valid business state, not a technical lie.

13. Retry Storm

Retry storm occurs when many clients retry at the same time and overload a weak dependency.

flowchart TD A[Dependency latency rises] --> B[Client timeouts] B --> C[Clients retry] C --> D[More traffic to dependency] D --> E[Dependency latency rises more] E --> B

Signals:

- dependency QPS increases while success rate decreases
- retry count spikes
- timeout rate spikes
- p95/p99 latency increases
- circuit breaker opens across many instances
- downstream CPU/connection pool saturated

Controls:

- retry budget
- exponential backoff + jitter
- circuit breaker
- rate limiting
- load shedding
- respecting Retry-After
- avoiding retry on overload signals

14. Circuit Breaker Flapping

Flapping means breaker repeatedly opens and closes.

Causes:

- sliding window too small
- threshold too sensitive
- half-open trial count too high/low
- timeout too aggressive
- dependency latency naturally bursty
- all instances half-open at same time

Mitigation:

- tune sliding window
- use slow-call threshold carefully
- apply jitter to recovery attempts if supported
- isolate by dependency operation
- observe breaker metrics before changing thresholds

Do not tune blindly.

Breaker config is a production control surface.

15. Bulkhead Starvation

Bulkhead protects one area but can also starve legitimate work if badly sized.

Example:

pricing bulkhead size = 5
normal traffic requires 20 concurrent calls
result: artificial bottleneck

Another example:

single shared executor for:
- order submit
- report generation
- reconciliation job

report job fills queue -> order submit waits -> customer-facing API degrades

Better:

- separate workload classes
- bounded queues
- priority or admission control
- metrics per pool
- rejection instead of unbounded waiting

16. Observability for Resilience

Minimum metrics:

- retry attempts count
- retry exhausted count
- retry success after retry
- circuit breaker state
- circuit breaker open count
- slow call rate
- failure rate
- bulkhead available permits
- bulkhead rejected calls
- dependency latency by operation
- dependency error by class

Important labels:

- dependency
- operation
- outcome
- exception class category
- retry attempt number bucket

Avoid high-cardinality labels:

- user id
- tenant id if too many and uncontrolled
- order id
- quote id
- raw URL with IDs
- exception message

Logs should answer:

- Was this original call or retry attempt?
- Which dependency operation failed?
- Was circuit breaker open?
- Was bulkhead full?
- Was fallback used?
- What correlation/trace ID connects it?

17. Example: Outbound Dependency Policy Matrix

DependencyOperationRetryCircuit breakerBulkheadFallback
catalog serviceread product catalogyes, limitedyesyescached catalog if allowed
pricing servicecalculate pricemaybeyesyesfail/pending depending workflow
payment/chargingcreate chargeno blind retryyesstrictidempotency-key required
PostgreSQLread querymaybe for transientno typical app breakerpool limitfail fast
PostgreSQLwrite transactiononly transaction-awareno typical app breakerpool limitno silent fallback
Kafka publishsend eventclient retry/outboxproducer metricsbuffer controloutbox replay
Redis cacheget valuemaybe tinyyes if criticalconnection poolcache miss path

This matrix should be internalized per service.


18. JAX-RS Exception Mapping for Resilience Failures

Resilience failures should map to stable API errors.

Examples:

Internal failureHTTP responseNotes
dependency timeout504 or 503depends whether service acts as gateway or processor
circuit breaker open503include retryability if policy allows
bulkhead full503 or 429overload/admission failure
retry exhausted503/504preserve root cause category
rate limited internally429include Retry-After if meaningful
fallback partial response200/206/domain-specificmust be explicit in contract

Do not leak raw exception names as API contract.

Bad:

{
  "error": "CallNotPermittedException"
}

Better:

{
  "code": "DEPENDENCY_UNAVAILABLE",
  "message": "A required downstream service is temporarily unavailable.",
  "retryable": true,
  "correlationId": "..."
}

19. Internal Verification Checklist

Use this when joining or reviewing an internal service.

Library / platform

  • Is Resilience4j used directly?
  • Is there an internal resilience wrapper library?
  • Is resilience applied by service mesh, API gateway, or generated client?
  • Are there annotation-based aspects?
  • Are policies centralized or per dependency?
  • Is there a platform standard for timeout/retry/circuit breaker?

Retry

  • Which exception/status codes are retried?
  • Is retry allowed only for idempotent operations?
  • Is there a retry budget?
  • Is backoff configured?
  • Is jitter configured?
  • Is Retry-After respected?
  • Are retries visible in metrics/logs/traces?

Circuit breaker

  • What is the breaker granularity: service, endpoint, operation, tenant?
  • What counts as failure?
  • What counts as slow call?
  • What are sliding window and threshold values?
  • How many half-open trial calls are allowed?
  • Are breaker state changes logged/alerted?

Bulkhead

  • Are thread pools shared across unrelated workloads?
  • Are queues bounded?
  • Are dependency calls isolated?
  • Are tenants isolated where required?
  • Are rejections mapped to stable errors?
  • Are bulkhead metrics visible?

Fallback

  • Which operations have fallback?
  • Is fallback a valid business state?
  • Is stale data allowed?
  • Is partial response explicit?
  • Is fallback usage observable?

20. PR Review Checklist

When reviewing a change that calls a dependency, ask:

Correctness
- Is the operation safe to retry?
- Are side effects idempotent or guarded?
- Does retry interact safely with transaction boundaries?
- Is fallback semantically valid?

Timeout and budget
- Is there a request deadline?
- Do attempts fit inside the deadline?
- Are retries bounded?
- Is backoff + jitter configured?

Circuit breaker
- Is breaker scoped correctly?
- Are failure classes correct?
- Could breaker flap?
- Is open breaker mapped to stable API error?

Bulkhead
- Is dependency concurrency bounded?
- Are queues bounded?
- Could this workload starve critical traffic?
- Are rejections observable?

Observability
- Are retry/breaker/bulkhead metrics emitted?
- Are logs correlated with trace/correlation ID?
- Can on-call see retry storm quickly?

Internal standard
- Does this follow platform resilience standard?
- Is Resilience4j/equivalent used consistently?
- Are configs externalized and reviewed?

21. Common Anti-Patterns

- Retrying every exception.
- Retrying non-idempotent writes without idempotency key.
- Retrying after client deadline expired.
- Same retry config for every dependency.
- Circuit breaker counting domain errors as dependency failure.
- Circuit breaker around local validation logic.
- Unbounded executor queue called a “bulkhead”.
- Fallback returning misleading business data.
- Retry hidden inside low-level library with no metrics.
- Gateway retry + application retry + client retry all enabled without budget.

The most dangerous pattern:

Everyone retries because everyone assumes someone else controls the retry budget.

22. Senior Mental Model

A resilient service is not one that always succeeds.

A resilient service is one that:

- fails quickly when continuing is harmful
- retries only when safe and useful
- protects critical resources
- isolates failure domains
- exposes accurate telemetry
- preserves domain correctness
- degrades explicitly, not silently

Retry is a scalpel, not a hammer.

Circuit breaker is a damage limiter, not a cure.

Bulkhead is a blast-radius boundary, not just a thread pool.


23. What This Enables Next

Setelah memahami retry, retry budget, circuit breaker, dan bulkhead, kita bisa masuk ke pattern proteksi overload yang lebih luas:

- rate limiting
- load shedding
- fallback strategy
- hedged request
- thundering herd prevention
- cascading failure containment
- graceful degradation

Itu menjadi fokus part berikutnya.

Lesson Recap

You just completed lesson 35 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.