5.2 KiB
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.
- Define Cacheable Data: Identify data suitable for caching (e.g., flyer data, recipe details, brand lists).
- Define Invalidation Strategy: Determine the cache invalidation strategy (e.g., time-to-live (TTL), event-based invalidation on data update).
- 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 implementationget<T>(key): Retrieve cached valueset<T>(key, value, ttl): Store value with TTLdel(key): Delete specific keyinvalidatePattern(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 listcache:flyers:{limit}:{offset}- Paginated flyer listscache:flyer:{id}- Individual flyer datacache:flyer-items:{flyerId}- Items for a specific flyercache:stats:*- Statistics datageocode:{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 allcache: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
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 servicesrc/services/db/flyer.db.ts- Repository with caching for brands, flyers, flyer itemssrc/services/flyerPersistenceService.server.ts- Cache invalidation on flyer creationsrc/routes/admin.routes.ts- Admin cache management endpointssrc/config/queryClient.ts- Client-side query cache configuration
Future Enhancements
- Recipe caching: Add caching to expensive recipe queries (by-sale-percentage, etc.)
- Cache warming: Pre-populate cache on startup for frequently accessed static data
- Cache metrics: Add hit/miss rate monitoring for observability
- Conditional caching: Skip cache for authenticated user-specific data
- Cache compression: Compress large cached payloads to reduce Redis memory usage