Circuit Breaker Fallback Strategies — Graceful Degradation When Services Fail
In this tutorial, you will learn about Circuit Breaker Fallback Strategies. We cover key concepts, practical examples, and best practices to help you master this topic.
Circuit breaker fallback strategies provide graceful degradation when downstream services fail, using static responses, cached data, degraded functionality, and fallback chains that maintain partial system availability during outages.
flowchart TD
R[Request] --> CB{Circuit Open?}
CB -->|No| Service[Call Service]
Service -->|Success| OK[Return Response]
Service -->|Fails| Fallback{Try Fallback}
CB -->|Yes| Fallback
Fallback -->|Cache| Cache[Return Cached Data]
Fallback -->|Default| Default[Return Default]
Fallback -->|Degraded| Degraded[Return Limited Response]
Fallback -->|All Fail| Error[Return Error]
What You'll Learn
- Static fallback responses
- Cache-based fallback strategies
- Degraded functionality modes
- Fallback chain escalation
- Stale data serving patterns
Why It Matters
Without fallbacks, any downstream service failure becomes a user-facing error. Fallbacks keep the application partially functional during outages, converting hard failures into degraded experiences that preserve core user workflows.
Real-World Use
DodaTech's product page uses a 3-tier fallback: (1) live product data from the catalog service, (2) cached data from Redis (5 minutes stale), (3) static product skeleton with "pricing currently unavailable" notice. Users can still browse and add to cart.
Static Fallback
import time
import random
class CircuitBreakerWithFallback:
def __init__(self, name, fallback_response=None):
self.name = name
self.fallback_response = fallback_response
self.failures = 0
self.state = 'CLOSED'
self.last_failure = 0
def call(self, fn, fallback=None, *args, **kwargs):
if self.state == 'OPEN':
if time.time() - self.last_failure > 30:
self.state = 'HALF_OPEN'
else:
return self._execute_fallback(fallback)
try:
result = fn(*args, **kwargs)
self.failures = 0
self.state = 'CLOSED'
return result
except Exception as e:
self.failures += 1
self.last_failure = time.time()
if self.failures >= 3:
self.state = 'OPEN'
return self._execute_fallback(fallback)
def _execute_fallback(self, fallback):
if fallback:
return fallback()
if self.fallback_response:
return self.fallback_response
return None
def get_user_preferences(user_id):
if random.random() < 0.6:
raise ConnectionError("Database unavailable")
return {"theme": "dark", "language": "en"}
cb = CircuitBreakerWithFallback("user-preferences", fallback_response={"theme": "light", "language": "en"})
for i in range(5):
result = cb.call(get_user_preferences, user_id=42)
print(f"Request {i+1}: {result}")
time.sleep(0.1)
Expected output:
Request 1: {"theme": "dark", "language": "en"}
Request 2: {"theme": "dark", "language": "en"}
Request 3: {"theme": "light", "language": "en"} (fallback)
Request 4: {"theme": "light", "language": "en"} (circuit open)
Request 5: {"theme": "light", "language": "en"} (circuit open)
Cache Fallback
import time
import json
class CacheFallback:
def __init__(self, ttl_seconds=60):
self.cache = {}
self.ttl = ttl_seconds
def get(self, key):
entry = self.cache.get(key)
if entry and time.time() - entry['time'] < self.ttl:
return entry['value']
return None
def set(self, key, value):
self.cache[key] = {'value': value, 'time': time.time()}
cache = CacheFallback(ttl_seconds=300)
def get_product_price(product_id):
if random.random() < 0.5:
raise Exception("Pricing service unavailable")
return {"product_id": product_id, "price": 29.99, "currency": "USD"}
def fetch_with_cache_fallback(product_id):
cache_key = f"price:{product_id}"
cached = cache.get(cache_key)
if cached:
return {**cached, "source": "cache"}
try:
price = get_product_price(product_id)
cache.set(cache_key, price)
return {**price, "source": "live"}
except Exception:
if cached:
return {**cached, "source": "stale_cache"}
return {"product_id": product_id, "price": None, "currency": "USD", "source": "unavailable"}
cache.set("price:1001", {"product_id": 1001, "price": 29.99, "currency": "USD"})
for i in range(4):
result = fetch_with_cache_fallback(1001)
print(f"Request {i+1}: source={result['source']}, price={result['price']}")
time.sleep(0.1)
Expected output:
Request 1: source=live, price=29.99
Request 2: source=stale_cache, price=29.99
Request 3: source=stale_cache, price=29.99
Request 4: source=cache, price=29.99
Fallback Chain
import time
class FallbackChain:
def __init__(self):
self.strategies = []
def add_strategy(self, name, fn):
self.strategies.append((name, fn))
return self
def execute(self):
errors = []
for name, fn in self.strategies:
try:
result = fn()
print(f"Fallback '{name}' succeeded")
return result
except Exception as e:
errors.append((name, str(e)))
print(f"Fallback '{name}' failed: {e}")
continue
raise Exception(f"All fallbacks failed: {errors}")
chain = FallbackChain()
def primary_service():
time.sleep(0.1)
raise ConnectionError("Primary service down")
def cache_service():
time.sleep(0.05)
return {"data": "cached", "stale_seconds": 30}
def static_default():
return {"data": "default", "notice": "Data may be outdated"}
chain.add_strategy("primary", primary_service)
chain.add_strategy("cache", cache_service)
chain.add_strategy("static", static_default)
result = chain.execute()
print(f"Final result: {result}")
Expected output:
Fallback 'primary' failed: Primary service down
Fallback 'cache' succeeded
Final result: {'data': 'cached', 'stale_seconds': 30}
Common Mistakes
- No fallback for read operations -- read operations without fallbacks cause hard errors for every downstream failure. Always provide fallback data for reads: cached values, defaults, or degraded responses.
- Fallback that calls the same failed service -- calling the same failing service in the fallback guarantees failure. Fallbacks must use different data sources, cached data, or static responses.
- Stale cache without staleness headers -- serving stale data without indicating staleness confuses users. Include "data_age" or "last_updated" in fallback responses. Set Cache-Control: stale-while-revalidate headers.
- Throwing exceptions in fallback code -- fallback code must never throw. If the fallback itself fails, the error propagates as a hard failure. Wrap fallback logic in try/except and return a minimal safe response.
- Single fallback for all endpoints -- each endpoint needs specific fallback behavior. Recommending incorrect data is worse than no data. Design fallbacks per use case: product pages show stale prices, but payment pages show hard errors.
Practice Questions
- What is the difference between static and cached fallback?
- How do you indicate that data is stale in a fallback response?
- What is a fallback chain and when would you use one?
- Why should fallback code never throw exceptions?
- How do you design fallbacks for write vs read operations?
Challenge
Build a multi-tier fallback system: (1) tier 1: live service call with circuit breaker (2 second timeout), (2) tier 2: Redis cache with configurable TTL per endpoint, (3) tier 3: local in-memory cache (10 second TTL) for frequently accessed data, (4) tier 4: static default values from configuration, (5) tier 5: error response with "service unavailable" status, (6) each fallback returns metadata about which tier served the request and data freshness, (7) Prometheus metrics for fallback activation rate per tier.
FAQ
Mini Project
Build a fallback management framework: (1) register fallback strategies per endpoint with priority ordering, (2) circuit breaker integration: trigger fallback chain when circuit is open, (3) configurable fallback per error type (timeout -> cache, 500 -> default, connection error -> stale cache), (4) fallback response wrapper that includes metadata (source tier, data age, freshness indicator), (5) A/B testing mode that serves fallback to a percentage of traffic to validate fallback behavior, (6) metrics dashboard showing fallback activation rates, tier distribution, and user impact metrics.
What's Next
Continue with Cache Integration to learn Caching strategies with circuit breakers. Then explore Resilience4j for Java-based circuit breaker implementations.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro