Back to Scenarios & Design Questions
Q21Question 21 of 67

A dependency has 99.9% availability but your API needs 99.99%: is that possible, and how?

Key Concepts
  • Availability composes multiplicatively along a synchronous path: your API cannot exceed its dependency's availability if every request needs it.
  • 99.9% = ~8.76 hours downtime/year; 99.99% = ~52.6 minutes; the math makes a synchronous 99.99% target impossible behind a 99.9% dependency.
  • Remove the dependency from the critical path via caching (serve recent results when dependency is down), replication (multiple independent instances), async workflows (durable accept, process later), or degraded functionality (reduced-but-valid response).
  • Not every request can be decoupled: an authoritative debit genuinely needs the source of truth, so raise that dependency's availability through replication.
  • Availability targets must be translated into concrete behavior: 'when X is down, Y still succeeds by doing Z.'
  • Two systems can both claim 99.99% while one collapses on dependency failure and the other degrades gracefully; only the behavior reveals the truth.
  • Test availability claims by injecting dependency failures and measuring.

Definition

You cannot exceed a synchronous dependency's availability -- to beat it, remove it from the critical path via caching, replication, async, or degradation, and express the target as concrete behavior, not just an SLA number.
  • Multiplicative Composition – availability along a synchronous chain is the product of each component's availability; more dependencies means lower overall availability.
  • Caching – serve recent results when the dependency is down, decoupling read availability from the dependency at the cost of staleness.
  • Replication – multiple independent instances of the dependency; combined availability is much higher because all must fail simultaneously.
  • Async Workflows – durably accept the request and return success immediately, complete the dependency interaction later with retries.
  • Degraded Functionality – return a reduced-but-valid response when the dependency is unavailable, defining the degraded response as a success.
  • Behavioral Translation – express targets as 'when X is unavailable, request type Y succeeds by doing Z' rather than just a number.

National Bank of India promises customers that balance checks succeed 99.99% of the time, but the central mainframe is only up 99.9% of the time. If every check must phone the mainframe and wait, the promise is mathematically impossible. So the bank keeps a recent copy of balances at the branch (caching), connects to two mainframes (replication), or tells the customer "here is your balance as of a minute ago" when the live system is unreachable (degradation).

Understanding the Concepts

Why is a synchronous 99.99% impossible behind a 99.9% dependency?

Availability composes multiplicatively along a synchronous critical path. If your API must call a dependency on every request, your availability is at most the dependency's availability.

JAVA
package com.bank.availability;

public class AvailabilityMath {
    public static void main(String[] args) {
        double depAvailability = 0.999;      // 99.9%
        double targetAvailability = 0.9999;  // 99.99%

        double depDowntimeHours = (1 - depAvailability) * 365 * 24;
        double targetDowntimeHours = (1 - targetAvailability) * 365 * 24;

        System.out.printf("Dependency: %.1f%% = %.1f hours downtime/year%n",
                depAvailability * 100, depDowntimeHours);
        System.out.printf("Target: %.2f%% = %.1f minutes downtime/year%n",
                targetAvailability * 100, targetDowntimeHours * 60);
        System.out.println();
        System.out.println("On a synchronous path: API availability <= dependency availability");
        System.out.println("Cannot promise 52.6 min downtime behind 8.76 hours of downtime");
        System.out.println("The target is a MATHEMATICAL CONTRADICTION on a sync path");
    }
}
TEXT
Dependency: 99.9% = 8.8 hours downtime/year
Target: 99.99% = 52.6 minutes downtime/year

On a synchronous path: API availability <= dependency availability
Cannot promise 52.6 min downtime behind 8.76 hours of downtime
The target is a MATHEMATICAL CONTRADICTION on a sync path

How does caching decouple availability?

Keep recent results so that when the dependency is down, reads still succeed from cache. During downtime, cached reads are served, decoupling read availability from the dependency.

JAVA
package com.bank.availability;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

public class CacheFallback {

    private final Map<String, BigDecimal> cache = new HashMap<>();
    private boolean ledgerAvailable = false; // dependency down

    public BigDecimal getBalance(String accountId) {
        if (ledgerAvailable) {
            BigDecimal live = new BigDecimal("75000.00"); // from ledger
            cache.put(accountId, live);
            return live;
        }
        BigDecimal cached = cache.get(accountId);
        if (cached != null) {
            System.out.println("Ledger DOWN: serving cached balance for " + accountId);
            return cached;
        }
        throw new RuntimeException("No cached value available for " + accountId);
    }

    public static void main(String[] args) {
        CacheFallback svc = new CacheFallback();
        svc.cache.put("SAV2001", new BigDecimal("75000.00")); // pre-populated
        System.out.println("Balance: " + svc.getBalance("SAV2001"));
    }
}
TEXT
Ledger DOWN: serving cached balance for SAV2001
Balance: 75000.00

How does replication raise effective availability?

Two independent 99.9% replicas with failover give combined availability of roughly 99.9999%, because both must fail simultaneously.

JAVA
package com.bank.availability;

public class ReplicationAvailability {
    public static void main(String[] args) {
        double singleAvailability = 0.999;
        double failureProbSingle = 1 - singleAvailability;

        // Both must fail simultaneously for outage
        double bothFailProb = failureProbSingle * failureProbSingle;
        double combinedAvailability = 1 - bothFailProb;

        System.out.printf("Single instance: %.3f%% (%.1f hours downtime/year)%n",
                singleAvailability * 100, failureProbSingle * 365 * 24);
        System.out.printf("Two independent replicas: %.6f%% (%.2f minutes downtime/year)%n",
                combinedAvailability * 100, bothFailProb * 365 * 24 * 60);
        System.out.println("Key word: INDEPENDENT. Shared failure points do not help.");
    }
}
TEXT
Single instance: 99.900% (8.8 hours downtime/year)
Two independent replicas: 99.999900% (5.26 minutes downtime/year)
Key word: INDEPENDENT. Shared failure points do not help.

How do async workflows remove the dependency from the success path?

Accept the request durably (write to a log or queue) and return success immediately. Complete the dependency interaction in the background with retries.

JAVA
package com.bank.availability;

import java.math.BigDecimal;

public class AsyncWorkflow {

    static void submitTransfer(String from, String to, BigDecimal amount) {
        // Step 1: durably accept (write to outbox/queue)
        System.out.println("Transfer accepted: " + from + " → " + to + " = " + amount);
        System.out.println("Written to durable outbox (success depends on outbox, not ledger)");

        // Step 2: background worker settles with ledger later, with retries
        System.out.println("Background: will settle with ledger when available");
    }

    public static void main(String[] args) {
        submitTransfer("SAV2001", "SAV3002", new BigDecimal("50000.00"));
        System.out.println("API returned 202 Accepted. Ledger availability does not matter here.");
    }
}
DIAGRAM
Transfer accepted: SAV2001 → SAV3002 = 50000.00
Written to durable outbox (success depends on outbox, not ledger)
Background: will settle with ledger when available
API returned 202 Accepted. Ledger availability does not matter here.

What is degraded functionality?

Return a reduced-but-valid response when the dependency is down, as long as you define the degraded response as a success.

JAVA
package com.bank.availability;

import java.math.BigDecimal;

public class DegradedFunctionality {

    static String getAccountDetails(String accountId, boolean ledgerAvailable) {
        if (ledgerAvailable) {
            return "Balance: 75000.00, Last 10 transactions: [...]"; // full response
        }
        // Degraded: return cached balance, omit transactions
        return "Balance: 75000.00 (cached), Transactions: unavailable";
    }

    public static void main(String[] args) {
        System.out.println("Full: " + getAccountDetails("SAV2001", true));
        System.out.println("Degraded: " + getAccountDetails("SAV2001", false));
        System.out.println("Both return HTTP 200 → both count as available");
    }
}
DIAGRAM
Full: Balance: 75000.00, Last 10 transactions: [...]
Degraded: Balance: 75000.00 (cached), Transactions: unavailable
Both return HTTP 200 → both count as available

Why must targets be translated into concrete behavior?

The number alone is empty. "99.99%" does not tell an engineer what to build. The behavior determines whether the target is actually achieved.

JAVA
package com.bank.availability;

public class BehavioralTranslation {
    public static void main(String[] args) {
        System.out.println("BAD: 'Our API has 99.99% availability' (just a number)");
        System.out.println();
        System.out.println("GOOD: Concrete behavior per request type:");
        System.out.println("  When ledger is DOWN:");
        System.out.println("    - Balance reads serve cached value (marked stale)");
        System.out.println("    - Transfer submissions are durably accepted, settled async");
        System.out.println("    - Authoritative debit checks use replicated ledger");
        System.out.println();
        System.out.println("Test by injecting ledger failure and measuring.");
    }
}
TEXT
BAD: 'Our API has 99.99% availability' (just a number)

GOOD: Concrete behavior per request type:
  When ledger is DOWN:
    - Balance reads serve cached value (marked stale)
    - Transfer submissions are durably accepted, settled async
    - Authoritative debit checks use replicated ledger

Test by injecting ledger failure and measuring.

What requests cannot be decoupled?

The authoritative debit genuinely needs the source of truth. For that operation, raise the dependency's availability through replication rather than pretending a stale cache is authoritative.

JAVA
package com.bank.availability;

// WRONG: using cached balance to authorize a debit
// cache might be stale → approve a debit the account cannot cover

// CORRECT: for the authoritative debit check, use a replicated ledger
// Two independent 99.9% replicas give 99.9999% combined
// The few truly synchronous operations get a hardened dependency

What are common interview follow-up questions?

  • How do you test that your availability claim holds under dependency failure?
  • What is the difference between availability and correctness in this context?
  • How do you decide which request types can tolerate staleness and which cannot?
  • How does an SLO error budget relate to this discussion?

Visual Overview

DIAGRAM
  Availability Composition
  ────────────────────────

  Synchronous path:
  API ──sync──► Dependency (99.9%)
  API availability: at most 99.9% (CANNOT exceed dependency)

  Remove dependency from critical path:

  1. Caching:
     API ──► Cache HIT → return (no dependency needed)
         └─► Cache MISS → dependency (only when cache misses)

  2. Replication:
     API ──► Replica A (99.9%) ──failover──► Replica B (99.9%)
     Combined: 99.9999% (both must fail)

  3. Async:
     API ──► Durable Accept (outbox) → return 202
             Background worker → dependency (later, with retries)

  4. Degradation:
     API ──► dependency DOWN → return cached/partial response (HTTP 200)
             dependency UP   → return full response

Key Takeaway

On a purely synchronous path, you cannot exceed a dependency's availability. A 99.9% dependency caps a synchronous caller at 99.9%, which is about nine hours of downtime a year, so a 99.99% target of about fifty-three minutes is a mathematical contradiction.

To beat it, remove the dependency from the critical path for at least some requests. Caching serves recent results during downtime. Replication with independent instances gives combined availability far higher than any single one. Async workflows durably accept the request and settle later. Degraded functionality returns a reduced-but-valid response. The overall target is met per request type: reads via caching, commands via async, and the few truly synchronous operations via a replicated dependency.

Targets must be translated into concrete behavior because the number alone is empty. "When the ledger is down, balance reads serve cached values and transfers are durably accepted" tells engineers exactly what to build and test. Prove each claim by injecting the dependency's failure and measuring.

Remember one sentence: you cannot exceed a synchronous dependency's availability, so remove it from the critical path via caching, replication, async, or degradation, and express the target as concrete behavior, not just a number.

---

Share
Use ← → arrow keys to navigate