When Things Break, Break Gracefully
No distributed system runs without failures. Dependencies go down, latency spikes, third-party APIs return 500s. The question isn't whether something will fail — it's whether your system handles that failure without dragging everything else down with it.
Two complementary techniques help here: graceful degradation (continuing to serve reduced functionality instead of nothing) and the circuit breaker pattern (stopping calls to a failing dependency before they cascade).
Graceful Degradation
Graceful degradation means your application stays partially useful when a dependency is unavailable. Instead of a blank page or a 500, the user sees a reduced but functional experience.
Common examples:
- A product page loads without personalized recommendations when the recommendation service is down.
- Search falls back to a simpler keyword query when the ML-ranking service times out.
- A checkout flow skips real-time fraud scoring and flags the order for manual review instead of blocking the purchase.
- Comments on a blog post fail silently while the article itself still loads.
The key design decision is identifying which features are core (must work) versus enhanced (nice to have). Draw that boundary early — retrofitting it is painful.
Designing for Degradation
- Map your dependencies. For each external call your service makes, decide: what should happen if this returns an error or times out?
- Set explicit timeouts. Never rely on a dependency's default timeout. Set your own, short ones. A 30-second timeout on a payment gateway means 30 seconds of user-facing hang before you can fall back.
- Return stale data where acceptable. Cache the last-known-good response and serve it when the live call fails. Make sure stale data is clearly acceptable for the use case — don't serve a cached stock price that's two hours old without labeling it.
- Signal degradation in your UI. Don't silently degrade in ways that confuse users. A small "recommendations unavailable" message is better than an empty section with no explanation.
- Test the degraded paths. Use fault injection (chaos engineering tools, or even a simple feature flag that forces a fallback) to verify your fallback actually works before a real outage forces it.
The Circuit Breaker Pattern
Graceful degradation handles what to do when a call fails. The circuit breaker handles whether to make the call at all.
The pattern comes from electrical engineering: when current exceeds a safe level, the breaker trips and cuts power rather than letting the wiring burn. In software, when a downstream service is failing repeatedly, you stop sending it requests — giving it time to recover and preventing your own thread pool or connection pool from exhausting itself waiting for responses that won't come.
Three States
- Closed — normal operation; requests flow through.
- Open — the breaker has tripped; requests fail immediately without hitting the downstream service.
- Half-open — after a configured timeout, a small number of probe requests are allowed through. If they succeed, the breaker closes. If they fail, it opens again.
Implementation Sketch
Most languages have mature circuit breaker libraries. A few well-known ones:
- Java / JVM: Resilience4j, Hystrix (now in maintenance mode)
- Go:
sony/gobreaker,rubyist/circuitbreaker - Python:
pybreaker - .NET: Polly
A minimal configuration requires three parameters: the failure threshold (e.g., 50% error rate over 10 requests), the open duration (e.g., 30 seconds before trying half-open), and the probe count in half-open state.
Avoid setting thresholds too sensitive — a single transient error shouldn't open the breaker. Use a sliding window over a meaningful sample size.
What to Do When the Breaker Is Open
An open breaker is just another failure path. Wire it into your graceful degradation logic:
- Return a cached response.
- Return a default/empty response.
- Queue the request for async retry.
- Surface a meaningful error to the caller.
Observability Is Not Optional
Neither pattern works well without visibility. Instrument your circuit breakers to emit metrics on state transitions, error rates, and fallback invocations. Alert when a breaker opens — it's a signal that a dependency is sick.
This is also where external monitoring earns its keep. Your internal metrics tell you what your code sees. A multi-region uptime monitor like Pingy tells you what your users see — whether the degraded experience is actually serving responses, and from which regions failures are visible. These are complementary, not redundant.
Key Takeaways
- Classify dependencies as core or enhanced, and define fallback behavior for each before you need it.
- Always set explicit, short timeouts — graceful degradation can't kick in until the failure is detected.
- Circuit breakers prevent cascade failures by stopping calls to a sick dependency proactively.
- Instrument state transitions and alert on open breakers; a breaker opening is an incident signal.
- Test your fallback paths deliberately — degraded modes are code paths that can have bugs too.
- External monitoring confirms what end users actually experience, independent of your internal instrumentation.