Dependency Failure and Graceful Degradation

LESSON

Reliability Engineering Foundations

008 25 min intermediate

Dependency Failure and Graceful Degradation

By the end of this lesson, you will be able to...

  • Classify dependencies by the part of the user promise they can break.

  • Design a degraded mode that preserves the most important promise when a dependency fails.

  • Review a dependency outage plan for fallback behavior, timeouts, retries, and honest user communication.

Idea in one sentence: Graceful degradation means choosing a smaller honest promise before a dependency failure turns into total failure.

Core Insight

The checkout service has learned several reliability habits.

It has a user promise. It has SLIs and SLOs. It pages on symptoms. It watches capacity, saturation, and safety margin.

Then the payment provider starts timing out.

At first, checkout is still "up." The API responds. The database is healthy. CPU is fine.

But every checkout request waits on the payment provider.

user clicks Pay
  -> checkout API validates cart
  -> checkout API reserves inventory
  -> checkout API calls payment provider
  -> payment provider times out
  -> checkout API waits, retries, waits again
  -> user sees spinner
  -> user clicks Pay again

One external dependency is failing, but the local service is now at risk too.

Workers stay occupied. Queues grow. Retries add load. Users lose a clear result. Duplicate-charge risk rises.

The naive design says:

If payment is down, checkout is down.

Sometimes that is true. You cannot honestly say an order is paid if payment is unknown.

But "we cannot complete the ideal path" does not always mean "we must fail everything." A better design asks:

What smaller promise can we keep safely?

For checkout, the smaller promise might be:

If payment confirmation is unavailable, give the user a clear pending state
within 2 minutes, do not charge twice, and reconcile later.

That is graceful degradation.

Plain meaning:

Graceful degradation means the service keeps a reduced, honest behavior when one part fails, instead of collapsing into confusing or unsafe behavior.

In this scenario:

Checkout stops promising "paid now" when the provider is unhealthy. It promises "safely pending, no duplicate charge, clear next state."

Technical name:

That reduced operating behavior is a degraded mode.

The Naive Dependency Model

A simple dependency diagram often looks like this:

checkout -> payment provider
checkout -> inventory service
checkout -> fraud service
checkout -> email service

The naive model treats each arrow as equally required.

If a dependency is used by checkout, checkout needs it.
If checkout needs it, failure should fail the request.
If failure fails the request, the only answer is to wait or retry.

That model creates two problems.

First, it hides different kinds of promises.

Payment authorization is not the same as sending a receipt email.

Inventory reservation is not the same as fraud enrichment.

A user-facing final state is not the same as an internal analytics event.

Second, it makes retries look like the only reliability tool.

Retries can help when a failure is brief and independent. They can hurt when the dependency is slow or overloaded. A retry that waits too long can consume local workers. A retry loop can turn one dependency failure into local saturation.

A better model classifies each dependency by the promise it protects.

Dependency If it fails, what promise is at risk? Can checkout degrade?
Payment provider User must not be charged incorrectly; checkout needs a truthful payment state. Yes, if checkout can move to a safe pending state and reconcile later.
Inventory service User should not buy unavailable items. Maybe, if inventory was reserved earlier or the product allows backorder.
Fraud service Risk checks may be incomplete. Often yes, by using stricter limits, manual review, or disabling high-risk orders.
Email service Receipt may be delayed. Yes, show receipt in app and send email later.
Analytics pipeline Business event may be delayed. Yes, buffer or drop according to policy; do not block checkout.

The design question is not:

Can this dependency fail?

It can.

The design question is:

When it fails, what exact user promise do we still keep?

Strong, Weak, and Optional Dependencies

A useful reliability review separates dependencies into three groups.

Strong dependency

The service cannot keep the core promise without this dependency.

Example:

Checkout cannot say "paid" without a trustworthy payment result.

Strong does not always mean "block forever." It means the ideal response is impossible without the dependency.

Weak dependency

The service can keep a smaller promise without this dependency.

Example:

Checkout cannot finish payment confirmation, but it can create a pending order
with an idempotency key and reconcile later.

Weak dependencies are where graceful degradation usually lives.

Optional dependency

The service can keep the user promise without this dependency.

Example:

Email receipt can be sent later.
Analytics can be buffered.
Recommendations can be hidden.

Optional dependencies should almost never block the core path during a failure.

Check: The fraud service is timing out. Low-risk orders can be marked pending for later review, but high-risk orders must not proceed. Is the fraud service strong, weak, or optional?

Think first, then reveal.

Answer: It is weak for some traffic and strong for other traffic. The degraded mode can allow low-risk orders into pending review while rejecting or delaying high-risk orders. Dependency strength can depend on request type and risk.

A Worked Degradation Plan

Design the degraded mode for a payment-provider outage.

Start with the normal promise:

Normal checkout promise:
  Users receive a clear result within 2 minutes:
  paid, declined, or safely pending.

Correctness promise:
  Users are not charged twice for one checkout attempt.

Now state the failure:

Dependency failure:
  payment provider timeout rate is above 30%
  provider p95 latency is above 8 seconds
  clear-result SLI is beginning to burn

The naive path is:

try payment
  -> wait
  -> timeout
  -> retry
  -> wait
  -> retry
  -> user waits
  -> worker stays occupied
  -> user retries manually
  -> duplicate risk increases

That path is dangerous because it creates both latency and correctness risk.

Now design the degraded path.

Design choice Degraded behavior
User promise "We received your order attempt. Payment confirmation is pending. You will not be charged twice for this checkout attempt."
Timeout Stop waiting for provider confirmation after a short bounded timeout, such as 2 seconds on the user request path.
Retry policy Do not retry many times in the user request. Move reconciliation to a controlled background process with idempotency keys and retry limits.
State Store payment_pending with order ID, idempotency key, provider attempt ID when available, and next reconciliation time.
User response Show a clear pending result, not an endless spinner and not a fake success.
Alert Page on clear-result SLO burn or duplicate-charge risk, not only on provider timeout rate.
Recovery Reconciler checks provider state later and moves order to paid, declined, or needs_support_review.
Stop condition If pending queue age grows beyond the promise, pause new payment attempts or disable the promotion.

Trace one request.

Input:

User U submits checkout attempt A with idempotency key K.

Transition:

Checkout validates the cart and writes an order attempt.
Checkout calls payment provider with key K.
Provider does not answer within the bounded timeout.

Intermediate state:

Order state:
  payment_pending

Stored evidence:
  user ID
  order ID
  idempotency key K
  provider attempt ID if known
  timeout reason
  next reconciliation time

Output:

User sees:
  "Payment is pending. Do not retry this checkout.
   We will update the order when confirmation arrives."

Later transition:

Reconciler asks provider about attempt K.
If provider accepted payment, order becomes paid.
If provider declined payment, order becomes declined.
If provider state is still unknown after a limit, order goes to support review.

Naive failure contrast:

Without the degraded mode, the request waits too long, retries too much,
occupies workers, and invites the user to retry manually.
That can break latency, availability, and correctness at the same time.

The degraded mode does not make the provider healthy. It protects the local promise from becoming dishonest or unsafe.

Circuit Breakers, Fallbacks, and Honest Responses

Three design tools often appear in degraded modes.

Timeout

A timeout says:

We will not wait forever.

Without a timeout, a slow dependency can consume workers until the local service saturates.

Circuit breaker

A circuit breaker says:

This dependency is failing enough that we should stop sending normal traffic
for a short period.

It protects the dependency from more load and protects the caller from waiting on a path that is probably unhealthy.

For checkout:

If provider timeouts exceed the threshold,
open the circuit for normal payment attempts.

While open:
  create safe pending states
  do not run repeated user-path retries
  allow controlled probe requests to test recovery

Fallback

A fallback says:

Here is the alternative behavior while the normal path is unavailable.

A fallback must be honest.

Bad fallback:

"Payment succeeded" when the payment state is unknown.

Good fallback:

"Payment is pending. We will confirm soon. Do not retry."

The fallback should preserve the most important part of the promise.

For checkout, correctness is more important than pretending everything is fast. A clear pending state is less convenient than immediate success, but it is safer than duplicate charges or ambiguous failure.

Check: Why is "show success and fix it later" a bad fallback for an unknown payment result?

Think first, then reveal.

Answer: It lies about state. If the payment later fails, the user saw a false success. If the user retries, the system may create duplicate risk. A degraded mode must be smaller than the normal promise, but still truthful.

Designing the Degraded Promise

A degraded mode needs a written promise.

Use this shape:

When <dependency or capability> is unhealthy,
the service will still <preserved promise>,
but it will not <normal capability that is unavailable>,
and users/operators will see <honest signal>.

For checkout:

When the payment provider is timing out,
checkout will still give users a clear pending state within 2 minutes
and protect idempotency for the checkout attempt,
but it will not promise immediate paid or declined status,
and users will see pending payment with a clear next step.

Now connect that promise to controls.

Control Why it exists
Short user-path timeout Prevents one slow dependency from consuming local workers.
Idempotency key Prevents duplicate charge attempts for one checkout action.
Pending state Gives the user an honest result instead of a spinner.
Background reconciler Moves uncertain payment state toward a final state later.
Retry cap Prevents recovery work from becoming overload.
Circuit breaker Stops sending normal traffic into a dependency that is probably failing.
Queue-age alert Detects when the degraded mode itself is falling behind.
Support review state Handles cases that remain ambiguous after automated reconciliation.

This is design work, not only operations work.

The degraded behavior must exist before the incident. During the incident, the team should be choosing and operating known modes, not inventing the state machine under pressure.

Trade-offs and Limits

Graceful degradation improves reliability because it avoids all-or-nothing behavior.

It can preserve a smaller but valuable promise while a dependency is slow, overloaded, or unavailable. It can also prevent retries from turning an external failure into local saturation.

It costs product clarity and engineering complexity.

You need extra states, user messages, reconciliation logic, idempotency, dashboards, and support flows. You also need to test the degraded path. An untested degraded mode is often just a hopeful diagram.

It does not solve every dependency failure.

Some dependencies are truly strong for some promises. If payment cannot be trusted, checkout cannot honestly say "paid." If inventory cannot be known for a scarce item, the service may need to stop selling that item.

You can see the boundary when:

the degraded queue grows without catching up
support cannot explain pending states to users
the fallback hides important risk
reconciliation creates duplicate or contradictory states
the dependency failure lasts longer than the business can tolerate

The trade-off is:

Failing closed protects correctness but may hurt availability.
Failing open preserves flow but may hurt correctness.
Graceful degradation chooses the smallest honest promise for the situation.

Common Confusions

Confusion: "Graceful degradation means users do not notice"

Why it is tempting:

The word "graceful" can sound like invisible.

Better model:

Users may notice a reduced experience. The goal is not invisibility. The goal is honest, bounded, safer behavior.

Confusion: "Fallback means fake the normal result"

Why it is tempting:

Teams want the product to feel smooth.

Better model:

A fallback must be truthful. It can return cached, partial, delayed, or pending information, but it should not claim certainty the system does not have.

Confusion: "Retries are the degraded mode"

Why it is tempting:

Retries are easy to add and sometimes fix transient failures.

Better model:

Retries are one control. A degraded mode also needs timeouts, limits, state, user communication, observability, and a recovery path.

Confusion: "Every dependency can be made optional"

Why it is tempting:

Optional dependencies make architecture feel safer.

Better model:

Some promises require specific dependencies. Reliability design is about naming which promises can shrink and which promises must stop rather than lie.

Practice

Review this dependency outage plan.

Dependency:
  fraud scoring service

Failure:
  timeout rate above 40%

Current plan:
  retry fraud scoring three times in the user request
  if all retries fail, approve the order
  send an alert to the fraud team later

Answer:

  1. What promise is at risk?
  2. What is unsafe about the current plan?
  3. What degraded mode would be safer?
  4. What signal should show that the degraded mode is reaching its limit?

Model answer:

1. The promise at risk is not only checkout speed. It is also safe ordering:
   the service should not approve high-risk orders without enough evidence.

2. The current plan waits too long in the user path and then fails open for
   every order. That may preserve short-term availability but can damage
   correctness, fraud risk, and business trust.

3. A safer degraded mode could approve only low-risk orders under stricter
   limits, move uncertain orders to manual review or pending review, reject
   clearly high-risk orders, and avoid repeated user-path retries.

4. Useful limit signals include pending-review queue age, percentage of orders
   entering degraded review, fraud-service timeout rate, and SLO burn for clear
   checkout result. If the pending-review queue grows too old, the degraded
   mode is no longer preserving the promise.

Resources

Key Takeaways

PREVIOUS Capacity, Saturation, and Safety Margins NEXT Incident Interfaces, Triage, and Handoff Boundaries