Combining Resilience Patterns: Decorator Order and Why It Matters
Fast recall
Quick revision
Core answer
Deep understanding of how resilience primitives compose; awareness that wrong order produces subtly broken behaviour under load
Putting Retry outside CircuitBreaker (retries bypass a tripped breaker and amplify load); putting TimeLimiter inside Retry (each attempt gets full timeout so total latency balloons)
Walk through what each wrong order breaks concretely — retry storms, deadline violations, bulkhead saturation — and show both the annotation-based order and the programmatic Decorators.ofSupplier fluent chain
Interview-ready guide
Interview answer
The canonical order
- TimeLimiter, CircuitBreaker, Retry, Bulkhead, RateLimiter, then the actual call — outermost to innermost
- Read it as: the outermost primitive sees everything inside it, and nothing outside
- Fallback attaches to the outermost decorator so it fires on any inner failure
Why TimeLimiter is outermost
- Users care about total latency, not per-attempt latency
- A 2-second deadline should cover all retries and all waits
- If TimeLimiter is inside Retry, each attempt gets a fresh 2 seconds and 3 retries blow the budget to 6 seconds
- Requires CompletableFuture because it runs work on an executor and cancels on deadline
Why CircuitBreaker sits above Retry
- When the breaker is OPEN, it rejects calls immediately
- If Retry were outside CircuitBreaker, the retry loop still runs three times bouncing off the breaker — wasted CPU and thread time
- CircuitBreaker outside Retry means one short-circuit ends the whole flow
- Also, CB sees one outcome per user-request, which is the user-visible failure signal
Why Retry sits above Bulkhead
- Each retry attempt acquires and releases its own bulkhead slot
- Backoff sleeps between attempts free slots for other callers
- If Bulkhead were outside Retry, one caller holds the slot across all attempts plus sleeps — a single retry chain starves the bulkhead
Why Bulkhead sits above RateLimiter
- Bulkhead decides if this attempt gets to try at all
- RateLimiter paces the actual outbound call
- Tokens are consumed only for real outbound traffic, not for bulkhead-rejected calls
Spring annotation reality
- Annotation declaration order does not change execution order
- Spring integration has a fixed runtime wrapping: TimeLimiter, CircuitBreaker, RateLimiter, Retry, Bulkhead
- To fully control order, use the programmatic Decorators.ofSupplier fluent API
Programmatic composition trap
- Decorators.ofSupplier chain is reversed conceptually
- You call withRateLimiter first, then withBulkhead, withRetry, withCircuitBreaker
- That's because each .with... wraps the previous supplier — innermost gets added first, outermost last
- Read it bottom-up if the inner-to-outer flow confuses you
What each wrong order concretely breaks
- Retry outside CircuitBreaker: retry storms bouncing off an open breaker; prolonged outages
- TimeLimiter inside Retry: latency overruns, deadline violations, downstream does 3x work
- Bulkhead outside Retry: single retry chain holds slot for seconds; other callers see BulkheadFullException
- Rate limiter outside retry: ambiguous; use it outside for per-user fairness, inside for downstream protection
Fallback placement
- Attach to the outermost decorator for a uniform catch-all
- Attaching per-primitive gives confusing partial-degradation behaviour
- Fallback should be fast, predictable, no external calls, log and metric every trigger
Metrics to watch for order bugs
- resilience4j_retry_calls successful_with_retry high while CB state is OPEN — retries hammering open breaker
- resilience4j_bulkhead_available_concurrent_calls near zero while CB is CLOSED — retry storm inside bulkhead
- resilience4j_timelimiter_calls timeout spiking with Retry counts — TimeLimiter should be outside Retry
The strategic answer
- Memorise the order: TimeLimiter, CircuitBreaker, Retry, Bulkhead, RateLimiter
- Use annotations for the common case; use Decorators.ofSupplier when order-proof matters
- Attach fallback once, on the outermost decorator
- Tune starting from 2-second TimeLimiter, 50% CB threshold, 3 retries with jitter, 10-slot bulkhead, downstream-quota RateLimiter
Combining Resilience Patterns: Decorator Order and Why It Matters
Direct Answer
Compose from outermost to innermost as TimeLimiter → CircuitBreaker → Retry → Bulkhead → RateLimiter → actual call. TimeLimiter bounds total wall-clock latency including retries. CircuitBreaker short-circuits when tripped so retries never waste capacity on doomed calls. Retry sits inside the breaker so its attempts contribute to the breaker's failure stats. Bulkhead caps concurrency per retry attempt. RateLimiter paces the actual outbound call. Wrong order causes retry storms that trip the breaker, missed deadlines, or bulkhead saturation from retry fan-out.
Common Mistakes & Pitfalls
Putting Retry outside CircuitBreaker (retries bypass a tripped breaker and amplify load); putting TimeLimiter inside Retry (each attempt gets full timeout so total latency balloons)
Check your understanding
Answer these without looking back at the article.
- How does annotation-declaration order compare to programmatic decorator order in Resilience4j?
- What happens if you put TimeLimiter inside Retry — what metric tells you it's wrong?
- Why must @TimeLimiter methods return CompletableFuture, and what is the risk of a mismatched HTTP readTimeout?
- How would you compose the same five patterns in a reactive WebClient chain?
Related interview prep categories
2Want a guided curriculum order?
These modules are also sequenced in our flagship backend interview track — open it when you prefer an ordered path instead of browsing by topic.