# Cache Infrastructure

Redis-backed caching system with CDC (Change Data Capture) event-driven invalidation.

## Architecture

### Core Components

```
┌─────────────────────────────────────────────────────────────┐
│                     Application Layer                        │
│  (Orders, Catalog, Invoices, etc. - Domain Logic)           │
└───────────────────────────┬─────────────────────────────────┘
                            │
┌───────────────────────────▼─────────────────────────────────┐
│              Domain-Specific Cache Services                  │
│  - OrdersCacheService (CDC-driven)                          │
│  - ServicesCacheService (CDC-driven + safety TTL)           │
│  - WhmcsCacheService (TTL-based)                            │
│                                                              │
│  Features:                                                   │
│  • Request coalescing (prevents thundering herd)            │
│  • Metrics tracking (hits/misses/invalidations)             │
│  • Dependency tracking (granular invalidation)              │
└───────────────────────────┬─────────────────────────────────┘
                            │
┌───────────────────────────▼─────────────────────────────────┐
│                     CacheService                             │
│         (Low-level Redis operations)                         │
│                                                              │
│  • get/set with JSON serialization                          │
│  • Pattern-based operations (scan/delete/count)             │
│  • Memory usage tracking                                     │
│  • Automatic error handling                                  │
└───────────────────────────┬─────────────────────────────────┘
                            │
┌───────────────────────────▼─────────────────────────────────┐
│                     Redis (ioredis)                          │
│         In-memory data store                                 │
└─────────────────────────────────────────────────────────────┘
```

### CDC Event-Driven Invalidation

```
┌─────────────────┐         ┌──────────────────┐
│   Salesforce    │ CDC     │  CDC Subscriber  │
│   (Data Source) │───────▶ │  (order-cdc,     │
│                 │ Events  │   catalog-cdc)   │
└─────────────────┘         └─────────┬────────┘
                                      │
                                      │ Invalidate
                                      ▼
                            ┌──────────────────┐
                            │  Cache Service   │
                            │  (Redis)         │
                            └──────────────────┘
```

## Cache Strategies

### 1. CDC-Driven (Orders, Catalog)

**Event-driven invalidation + safety TTL** - Cache is invalidated on CDC events, and also expires after a long TTL as a safety net.

Why: CDC is the primary freshness mechanism, but a safety TTL helps self-heal if events are missed (deploy downtime, subscriber issues, replay gaps).

Config:

- `SERVICES_CACHE_SAFETY_TTL_SECONDS` (default: 12 hours, set to `0` to disable)

**Pros:**

- Real-time invalidation when data changes
- Zero stale data for customer-visible fields
- Optimal for frequently read, infrequently changed data

**Example:**

```typescript
@Injectable()
export class OrdersCacheService {
  // CDC invalidation + safety TTL (service-specific)
  async getOrderSummaries(
    sfAccountId: string,
    fetcher: () => Promise<OrderSummary[]>
  ): Promise<OrderSummary[]> {
    const key = this.buildAccountKey(sfAccountId);
    return this.getOrSet("summaries", key, fetcher, false);
  }
}
```

### 2. TTL-Based (WHMCS, External APIs)

**Fixed TTL** - Cache expires after a set duration.

**Pros:**

- Simple, predictable behavior
- Good for external systems without CDC
- Automatic cleanup of stale data

**Example:**

```typescript
@Injectable()
export class WhmcsCacheService {
  private readonly INVOICES_TTL = 90; // 90 seconds

  async getInvoicesList(...): Promise<InvoiceList | null> {
    return this.cacheService.getOrSet(key, fetcher, this.INVOICES_TTL);
  }
}
```

### 3. Hybrid (Volatile Data)

**Short TTL + CDC** - Combines both approaches.

**Use case:** Frequently changing data that also needs CDC invalidation.

```typescript
private readonly VOLATILE_TTL = 60; // 1 minute
```

## Key Features

### Request Coalescing

Prevents "thundering herd" problem when cache is invalidated and multiple requests arrive simultaneously.

```typescript
// When 100 users request data after CDC invalidation:
// ❌ Without coalescing: 100 API calls
// ✅ With coalescing: 1 API call, shared by all 100 users

private readonly inflightRequests = new Map<string, Promise<unknown>>();

// Check if another request is already fetching this data
const existingRequest = this.inflightRequests.get(key);
if (existingRequest) {
  return existingRequest; // Wait for and share the existing request
}
```

### Dependency Tracking (Catalog Only)

Enables granular invalidation by tracking relationships between cached data and source entities.

```typescript
// When Product A changes:
// ✅ Invalidate only catalog entries that depend on Product A
// ❌ Don't invalidate unrelated Product B entries

await catalogCache.invalidateProducts(["Product_A_ID"]);
```

### Metrics Tracking

All cache services track performance metrics:

```typescript
{
  services: { hits: 1250, misses: 48 },
  static: { hits: 890, misses: 12 },
  volatile: { hits: 450, misses: 120 },
  invalidations: 15
}
```

Access via health endpoints:

- `GET /api/health/services/cache`
- `GET /health`

## Creating a New Cache Service

### Step 1: Import Shared Types

```typescript
import { CacheService } from "@bff/infra/cache/cache.service";
import type { CacheBucketMetrics } from "@bff/infra/cache/cache.types";
```

### Step 2: Define Service Structure

```typescript
@Injectable()
export class MyDomainCacheService {
  private readonly metrics = {
    data: { hits: 0, misses: 0 },
    invalidations: 0,
  };

  // Request coalescing map
  private readonly inflightRequests = new Map<string, Promise<unknown>>();

  constructor(private readonly cache: CacheService) {}
}
```

### Step 3: Implement Get/Set Pattern

```typescript
async getMyData(id: string, fetcher: () => Promise<MyData>): Promise<MyData> {
  const key = `mydomain:${id}`;

  // Check cache
  const cached = await this.cache.get<MyData>(key);
  if (cached) {
    this.metrics.data.hits++;
    return cached;
  }

  // Check for in-flight request
  const existing = this.inflightRequests.get(key);
  if (existing) return existing as Promise<MyData>;

  // Fetch fresh data
  this.metrics.data.misses++;
  const fetchPromise = (async () => {
    try {
      const fresh = await fetcher();
      await this.cache.set(key, fresh); // CDC-driven (TTL varies by domain)
      return fresh;
    } finally {
      this.inflightRequests.delete(key);
    }
  })();

  this.inflightRequests.set(key, fetchPromise);
  return fetchPromise;
}
```

### Step 4: Implement Invalidation

```typescript
async invalidateMyData(id: string): Promise<void> {
  this.metrics.invalidations++;
  await this.cache.del(`mydomain:${id}`);
}
```

### Step 5: Wire Up CDC Subscriber (if needed)

```typescript
@Injectable()
export class MyDomainCdcSubscriber implements OnModuleInit {
  constructor(private readonly myCache: MyDomainCacheService) {}

  private async handleEvent(data: unknown): Promise<void> {
    const id = extractId(data);
    await this.myCache.invalidateMyData(id);
  }
}
```

## Cache Key Conventions

Use colon-separated hierarchical keys:

```
domain:type:identifier[:subkey]
```

Examples:

- `orders:account:001xx000003EgI1AAK`
- `orders:detail:80122000000D4UGAA0`
- `services:internet:acc_001:jp`
- `services:deps:product:01t22000003xABCAA2`
- `mapping:userId:user_12345`

## Configuration

### Environment Variables

```env
# Redis connection
REDIS_URL=redis://localhost:6379

# Production (Docker Compose)
REDIS_URL=redis://cache:6379/0
```

### Redis Module

Location: `/infra/redis/redis.module.ts`

Provides global `REDIS_CLIENT` using ioredis.

## Monitoring

### Health Checks

```bash
# Overall system health (includes Redis check)
GET /health

# Services cache metrics
GET /api/health/services/cache
```

### Response Format

```json
{
  "timestamp": "2025-11-18T10:30:00.000Z",
  "metrics": {
    "catalog": { "hits": 1250, "misses": 48 },
    "static": { "hits": 890, "misses": 12 },
    "invalidations": 15
  },
  "ttl": {
    "servicesSeconds": null,
    "staticSeconds": null,
    "volatileSeconds": 60
  }
}
```

## Best Practices

### ✅ DO

- Use CDC invalidation for Salesforce data
- Implement request coalescing for all cache services
- Track metrics (hits/misses/invalidations)
- Use structured, hierarchical cache keys
- Document TTL strategy in comments
- Add JSDoc comments to public methods

### ❌ DON'T

- Mix TTL and CDC strategies without clear reasoning
- Cache sensitive data without encryption
- Use cache as primary data store
- Forget to clean up in-flight requests (use `finally` block)
- Use overly generic cache keys (risk of collision)

## Troubleshooting

### High Cache Miss Rate

1. Check CDC subscriber health: `GET /health/sf-events`
2. Verify invalidation patterns match expected keys
3. Review TTL settings (if applicable)

### Stale Data Issues

1. Confirm CDC events are being received
2. Check invalidation logic in CDC subscribers
3. Verify cache key construction matches between set/invalidate

### Memory Issues

```typescript
// Check memory usage
const usage = await cacheService.memoryUsageByPattern("orders:*");
const count = await cacheService.countByPattern("orders:*");

console.log(`${count} keys using ${usage} bytes`);
```

## Related Documentation

- [Salesforce CDC Events](../../integrations/salesforce/events/README.md)
- [Order Fulfillment Flow](../../modules/orders/docs/FULFILLMENT.md)
- [Redis Configuration](../redis/README.md)
