# Fallback Pattern

## Problem

When a service call fails (timeout, error, circuit open), returning an error to the user gives a poor experience — especially if an approximate or cached result would be acceptable. Without a fallback strategy, every failure is a hard failure.

## Solution

Define an alternative behavior for when the primary operation fails. The fallback provides degraded but functional service rather than a complete failure. Common fallback strategies:

- **Cached value** — Return the last known good result.
- **Default value** — Return a sensible static default.
- **Alternative service** — Call a backup or simpler service.
- **Graceful degradation** — Omit the feature entirely.

## When to Use

- Read operations where stale data is better than no data
- Non-critical features that can be omitted without breaking the core flow
- Services with known backup providers
- Any operation wrapped in a circuit breaker or timeout

## When to Avoid

- Write operations where correctness is critical (don't silently skip writes)
- Financial transactions where approximate values are dangerous
- Operations where the fallback could mask a systemic issue
- When there's no meaningful degraded behavior

## Pseudocode

```
function with_fallback(primary_operation, fallback_operation):
    try:
        return primary_operation()
    catch error:
        log_warning("Primary failed, using fallback", error)
        return fallback_operation()
```

## Python Implementation

```python
import time
from typing import Callable, Any, Optional


class FallbackHandler:
    def __init__(self, name: str):
        self.name = name
        self.cache: dict[str, Any] = {}

    def with_fallback(
        self,
        primary: Callable[[], Any],
        fallback: Callable[[], Any],
        cache_key: Optional[str] = None,
    ) -> Any:
        try:
            result = primary()
            if cache_key:
                self.cache[cache_key] = result
            return result
        except Exception as exc:
            error_msg = str(exc)
            print(f"  [Fallback:{self.name}] Primary failed: {error_msg}")
            return fallback()

    def with_cache_fallback(
        self,
        primary: Callable[[], Any],
        cache_key: str,
        default: Any = None,
    ) -> Any:
        def fallback():
            if cache_key in self.cache:
                print(f"  [Fallback:{self.name}] Returning cached value")
                return self.cache[cache_key]
            print(f"  [Fallback:{self.name}] No cache — returning default")
            return default

        return self.with_fallback(primary, fallback, cache_key)


# --- Demo ---
handler = FallbackHandler("PricingService")

call_count = 0

def get_live_price():
    global call_count
    call_count += 1
    if call_count > 1:
        raise ConnectionError("Pricing service unavailable")
    return {"item": "Widget", "price": 29.99, "currency": "USD"}


# First call succeeds and caches
print("=== Call 1: Primary succeeds (result cached) ===")
result = handler.with_cache_fallback(
    get_live_price, cache_key="widget_price", default={"item": "Widget", "price": 0, "currency": "USD"}
)
print(f"  Result: {result}")

print()

# Second call fails, falls back to cache
print("=== Call 2: Primary fails, cache fallback ===")
result = handler.with_cache_fallback(
    get_live_price, cache_key="widget_price", default={"item": "Widget", "price": 0, "currency": "USD"}
)
print(f"  Result: {result}")

print()

# No cache available, falls back to default
print("=== Call 3: Primary fails, no cache, default fallback ===")
result = handler.with_cache_fallback(
    get_live_price, cache_key="new_item_price", default={"item": "Unknown", "price": 0, "currency": "USD"}
)
print(f"  Result: {result}")

print()

# Custom fallback function
print("=== Call 4: Custom fallback (alternative service) ===")
result = handler.with_fallback(
    primary=lambda: (_ for _ in ()).throw(ConnectionError("Main API down")),
    fallback=lambda: {"item": "Widget", "price": 25.00, "source": "backup-api"},
)
print(f"  Result: {result}")
```

**Output:**
```
=== Call 1: Primary succeeds (result cached) ===
  Result: {'item': 'Widget', 'price': 29.99, 'currency': 'USD'}

=== Call 2: Primary fails, cache fallback ===
  [Fallback:PricingService] Primary failed: Pricing service unavailable
  [Fallback:PricingService] Returning cached value
  Result: {'item': 'Widget', 'price': 29.99, 'currency': 'USD'}

=== Call 3: Primary fails, no cache, default fallback ===
  [Fallback:PricingService] Primary failed: Pricing service unavailable
  [Fallback:PricingService] No cache — returning default
  Result: {'item': 'Unknown', 'price': 0, 'currency': 'USD'}

=== Call 4: Custom fallback (alternative service) ===
  [Fallback:PricingService] Primary failed: Main API down
  Result: {'item': 'Widget', 'price': 25.0, 'source': 'backup-api'}
```

## Go Implementation

```go
package main

import (
	"errors"
	"fmt"
	"sync"
)

type FallbackHandler struct {
	name  string
	cache map[string]interface{}
	mu    sync.RWMutex
}

func NewFallbackHandler(name string) *FallbackHandler {
	return &FallbackHandler{name: name, cache: make(map[string]interface{})}
}

func (fh *FallbackHandler) WithFallback(primary, fallback func() (interface{}, error)) (interface{}, error) {
	result, err := primary()
	if err == nil {
		return result, nil
	}
	fmt.Printf("  [Fallback:%s] Primary failed: %v\n", fh.name, err)
	return fallback()
}

func (fh *FallbackHandler) WithCacheFallback(primary func() (interface{}, error), cacheKey string, defaultVal interface{}) interface{} {
	result, err := primary()
	if err == nil {
		fh.mu.Lock()
		fh.cache[cacheKey] = result
		fh.mu.Unlock()
		return result
	}
	fmt.Printf("  [Fallback:%s] Primary failed: %v\n", fh.name, err)
	fh.mu.RLock()
	cached, ok := fh.cache[cacheKey]
	fh.mu.RUnlock()
	if ok {
		fmt.Printf("  [Fallback:%s] Returning cached value\n", fh.name)
		return cached
	}
	fmt.Printf("  [Fallback:%s] No cache — returning default\n", fh.name)
	return defaultVal
}

func main() {
	handler := NewFallbackHandler("PricingService")
	callCount := 0

	getLivePrice := func() (interface{}, error) {
		callCount++
		if callCount > 1 {
			return nil, errors.New("pricing service unavailable")
		}
		return map[string]interface{}{"item": "Widget", "price": 29.99}, nil
	}

	fmt.Println("=== Call 1: Primary succeeds (result cached) ===")
	result := handler.WithCacheFallback(getLivePrice, "widget", map[string]interface{}{"price": 0})
	fmt.Printf("  Result: %v\n\n", result)

	fmt.Println("=== Call 2: Primary fails, cache fallback ===")
	result = handler.WithCacheFallback(getLivePrice, "widget", map[string]interface{}{"price": 0})
	fmt.Printf("  Result: %v\n\n", result)

	fmt.Println("=== Call 3: Primary fails, no cache, default ===")
	result = handler.WithCacheFallback(getLivePrice, "new_item", map[string]interface{}{"price": 0})
	fmt.Printf("  Result: %v\n", result)
}
```

## JavaScript Implementation

```javascript
class FallbackHandler {
  constructor(name) {
    this.name = name;
    this.cache = new Map();
  }

  async withFallback(primary, fallback, cacheKey) {
    try {
      const result = await primary();
      if (cacheKey) this.cache.set(cacheKey, result);
      return result;
    } catch (err) {
      console.log(`  [Fallback:${this.name}] Primary failed: ${err.message}`);
      return fallback();
    }
  }

  async withCacheFallback(primary, cacheKey, defaultValue) {
    return this.withFallback(
      primary,
      () => {
        if (this.cache.has(cacheKey)) {
          console.log(`  [Fallback:${this.name}] Returning cached value`);
          return this.cache.get(cacheKey);
        }
        console.log(`  [Fallback:${this.name}] No cache — returning default`);
        return defaultValue;
      },
      cacheKey
    );
  }
}

// --- Demo ---
const handler = new FallbackHandler("PricingService");
let callCount = 0;

function getLivePrice() {
  callCount++;
  if (callCount > 1) throw new Error("Pricing service unavailable");
  return { item: "Widget", price: 29.99, currency: "USD" };
}

(async () => {
  console.log("=== Call 1: Primary succeeds (result cached) ===");
  let result = await handler.withCacheFallback(
    () => getLivePrice(), "widget_price", { item: "Unknown", price: 0 }
  );
  console.log("  Result:", JSON.stringify(result));

  console.log("\n=== Call 2: Primary fails, cache fallback ===");
  result = await handler.withCacheFallback(
    () => getLivePrice(), "widget_price", { item: "Unknown", price: 0 }
  );
  console.log("  Result:", JSON.stringify(result));

  console.log("\n=== Call 3: Primary fails, no cache, default ===");
  result = await handler.withCacheFallback(
    () => getLivePrice(), "new_item", { item: "Unknown", price: 0 }
  );
  console.log("  Result:", JSON.stringify(result));
})();
```

## Related Patterns

- **Circuit Breaker** — circuit breaker trips when failures exceed a threshold; the fallback is what executes when the breaker is open.
- **Null Object** — a null fallback (returning a safe empty/default value) is an application of the Null Object pattern.
- **Strategy** — the fallback is a strategy: swap primary and fallback implementations at runtime.
- **Cache** — a stale cache entry is one of the most useful fallback strategies for read operations.
