# 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, no TTL) │ │ - CatalogCacheService (CDC-driven, no 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) **No TTL** - Cache persists indefinitely until CDC event triggers invalidation. **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 { // No TTL = CDC-only invalidation async getOrderSummaries( sfAccountId: string, fetcher: () => Promise ): Promise { 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 { 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>(); // 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 { catalog: { hits: 1250, misses: 48 }, static: { hits: 890, misses: 12 }, volatile: { hits: 450, misses: 120 }, invalidations: 15 } ``` Access via health endpoints: - `GET /health/catalog/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>(); constructor(private readonly cache: CacheService) {} } ``` ### Step 3: Implement Get/Set Pattern ```typescript async getMyData(id: string, fetcher: () => Promise): Promise { const key = `mydomain:${id}`; // Check cache const cached = await this.cache.get(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; // Fetch fresh data this.metrics.data.misses++; const fetchPromise = (async () => { try { const fresh = await fetcher(); await this.cache.set(key, fresh); // No TTL = CDC-driven return fresh; } finally { this.inflightRequests.delete(key); } })(); this.inflightRequests.set(key, fetchPromise); return fetchPromise; } ``` ### Step 4: Implement Invalidation ```typescript async invalidateMyData(id: string): Promise { 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 { 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` - `catalog:internet:acc_001:jp` - `catalog: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 # Catalog cache metrics GET /health/catalog/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": { "catalogSeconds": 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)