# ADR-009: Caching Strategy for Read-Heavy Operations

**Date**: 2025-12-12

**Status**: Accepted

## Context

The application has several read-heavy endpoints (e.g., getting flyer items, recipes, brands). As traffic increases, these endpoints will put significant load on the database, even for data that changes infrequently.

## Decision

We will implement a multi-layered caching strategy using an in-memory data store like **Redis**.

1. **Define Cacheable Data**: Identify data suitable for caching (e.g., flyer data, recipe details, brand lists).
2. **Define Invalidation Strategy**: Determine the cache invalidation strategy (e.g., time-to-live (TTL), event-based invalidation on data update).
3. **Implement Cache-Aside Pattern**: The repository layer will be updated to implement a "Cache-Aside" pattern, where methods first check Redis for data before falling back to the database.

## Consequences

**Positive**: Directly addresses application performance and scalability. Reduces database load and improves API response times for common requests.
**Negative**: Introduces Redis as a dependency if not already used. Adds complexity to the data-fetching logic and requires careful management of cache invalidation to prevent stale data.

## Implementation Details

### Cache Service

A centralized cache service (`src/services/cacheService.server.ts`) provides reusable caching functionality:

- **`getOrSet<T>(key, fetcher, options)`**: Cache-aside pattern implementation
- **`get<T>(key)`**: Retrieve cached value
- **`set<T>(key, value, ttl)`**: Store value with TTL
- **`del(key)`**: Delete specific key
- **`invalidatePattern(pattern)`**: Delete keys matching a pattern

All cache operations are fail-safe - cache failures do not break the application.

### TTL Configuration

Different data types use different TTL values based on volatility:

| Data Type           | TTL       | Rationale                              |
| ------------------- | --------- | -------------------------------------- |
| Brands/Stores       | 1 hour    | Rarely changes, safe to cache longer   |
| Flyer lists         | 5 minutes | Changes when new flyers are added      |
| Individual flyers   | 10 minutes| Stable once created                    |
| Flyer items         | 10 minutes| Stable once created                    |
| Statistics          | 5 minutes | Can be slightly stale                  |
| Frequent sales      | 15 minutes| Aggregated data, updated periodically  |
| Categories          | 1 hour    | Rarely changes                         |

### Cache Key Strategy

Cache keys follow a consistent prefix pattern for pattern-based invalidation:

- `cache:brands` - All brands list
- `cache:flyers:{limit}:{offset}` - Paginated flyer lists
- `cache:flyer:{id}` - Individual flyer data
- `cache:flyer-items:{flyerId}` - Items for a specific flyer
- `cache:stats:*` - Statistics data
- `geocode:{address}` - Geocoding results (30-day TTL)

### Cached Endpoints

The following repository methods implement server-side caching:

| Method | Cache Key Pattern | TTL |
| ------ | ----------------- | --- |
| `FlyerRepository.getAllBrands()` | `cache:brands` | 1 hour |
| `FlyerRepository.getFlyers()` | `cache:flyers:{limit}:{offset}` | 5 minutes |
| `FlyerRepository.getFlyerItems()` | `cache:flyer-items:{flyerId}` | 10 minutes |

### Cache Invalidation

**Event-based invalidation** is triggered on write operations:

- **Flyer creation** (`FlyerPersistenceService.saveFlyer`): Invalidates all `cache:flyers*` keys
- **Flyer deletion** (`FlyerRepository.deleteFlyer`): Invalidates specific flyer and flyer items cache, plus flyer lists

**Manual invalidation** via admin endpoints:

- `POST /api/admin/system/clear-cache` - Clears all application cache (flyers, brands, stats)
- `POST /api/admin/system/clear-geocode-cache` - Clears geocoding cache

### Client-Side Caching

TanStack React Query provides client-side caching with configurable stale times:

| Query Type        | Stale Time  |
| ----------------- | ----------- |
| Categories        | 1 hour      |
| Master Items      | 10 minutes  |
| Flyer Items       | 5 minutes   |
| Flyers            | 2 minutes   |
| Shopping Lists    | 1 minute    |
| Activity Log      | 30 seconds  |

### Multi-Layer Cache Architecture

```text
Client Request
      ↓
[TanStack React Query] ← Client-side cache (staleTime-based)
      ↓
[Express API]
      ↓
[CacheService.getOrSet()] ← Server-side Redis cache (TTL-based)
      ↓
[PostgreSQL Database]
```

## Key Files

- `src/services/cacheService.server.ts` - Centralized cache service
- `src/services/db/flyer.db.ts` - Repository with caching for brands, flyers, flyer items
- `src/services/flyerPersistenceService.server.ts` - Cache invalidation on flyer creation
- `src/routes/admin.routes.ts` - Admin cache management endpoints
- `src/config/queryClient.ts` - Client-side query cache configuration

## Future Enhancements

1. **Recipe caching**: Add caching to expensive recipe queries (by-sale-percentage, etc.)
2. **Cache warming**: Pre-populate cache on startup for frequently accessed static data
3. **Cache metrics**: Add hit/miss rate monitoring for observability
4. **Conditional caching**: Skip cache for authenticated user-specific data
5. **Cache compression**: Compress large cached payloads to reduce Redis memory usage
