The macro tracking feature implements a hybrid caching strategy using both Redis/Cachified and Prisma Accelerate to optimize performance while managing costs effectively. This document explains how caching works, when caches are invalidated, and how to maintain the system.
Redis Cache (via Cachified) - For frequently changing data
Prisma Accelerate - For stable data
All cache keys are centralized in macro-tracking-cache-keys.ts:
const MacroTrackingCacheKeys = {
// Redis cache keys (date-specific for granular invalidation)
userLogsDate: (userId: number, date: string) =>
`macro:user:${userId}:logs:${date}`,
// Prisma Accelerate tags
tags: {
user: (userId: number) => `user_${userId}`,
userFood: (userId: number) => `user_${userId}_food`,
userScheduled: (userId: number) => `user_${userId}_scheduled`,
userScheduledDate: (userId: number, date: string) =>
`user_${userId}_scheduled_${date}`,
// ... more tags
},
};
This table shows exactly which caches are invalidated for each operation:
| Operation | Service Function | Redis Cache Keys | Prisma Accelerate Tags |
|-----------|-----------------|------------------|------------------------|
| Create Food Log | createFoodLogsWithInvalidation | macro:user:{userId}:logs:{date} (for each unique date) | None |
| Create Recipe Log | createFoodLogsWithInvalidation | macro:user:{userId}:logs:{date} (for each unique date) | None |
| Update Food Log | updateFoodLogWithInvalidation | macro:user:{userId}:logs:{originalDate}, macro:user:{userId}:logs:{newDate} (if date changed) | None |
| Update Recipe Log | updateFoodLogWithInvalidation | macro:user:{userId}:logs:{originalDate}, macro:user:{userId}:logs:{newDate} (if date changed) | None |
| Delete Food Log | deleteFoodLogWithInvalidation | macro:user:{userId}:logs:{date} | None |
| Delete Recipe Log | deleteFoodLogWithInvalidation | macro:user:{userId}:logs:{date} | None |
| Complete Day | markDayCompleteWithInvalidation | macro:user:{userId}:logs:{date} | user_{userId}_completion, user_{userId}_streaks |
| Schedule Meal Plan | scheduleMealPlanWithInvalidation | - | user_{userId}_scheduled |
| Delete Scheduled Meal | deleteScheduledMealWithInvalidation | - | user_{userId}_scheduled, user_{userId}_scheduled_{date} |
*.service.server.ts), not in route handlersforceFresh: true to trigger background refresh without blocking| Action | Redis Invalidation | Prisma Tags Invalidated |
| --------------- | -------------------- | ----------------------------------------------------------- |
| Create Food Log | Date-specific cache | user_food, user_logs, user_scheduled_date (if recipe) |
| Edit Food Log | Original & new dates | user_food, user_logs |
| Delete Food Log | Date-specific cache | user_food, user_logs, user_scheduled_date (if recipe) |
| Complete Day | Date-specific cache | user_completion, user_streaks |
// Example: Getting food logs with Redis caching
async function getUserFoodLogsForDateCached(
userId: number,
date: string,
forceFresh = false
) {
return cachified({
key: MacroTrackingCacheKeys.userLogsDate(userId, date),
cache: redisCache,
forceFresh,
checkValue: loggedFoodItemArraySchema, // Zod validation
ttl: ONE_MINUTE * 5,
swr: ONE_MINUTE * 15,
async getFreshValue() {
return getUserFoodLogsForDateDB(userId, date);
},
});
}
// Example: Creating logs with automatic cache invalidation
async function createFoodLogsWithInvalidation(
userId: number,
logData: CreateFoodLogData[],
foodIdsToIncrement: number[]
) {
// 1. Perform database operation
await createFoodLogsTransactionDB(userId, logData, foodIdsToIncrement);
// 2. Extract unique dates from log data for cache invalidation
const dates = [...new Set(logData.map((log) => log.loggedDate))];
// 3. Invalidate Redis caches (background refresh)
const redisInvalidations = dates.map((date) =>
getUserFoodLogsForDateCached(userId, date, true)
);
// 4. Execute Redis invalidations
await Promise.all(redisInvalidations);
}
// Example: Prisma Accelerate caching
async function getActiveScheduledPlanDB(userId: number, date: string) {
return db.scheduledMealPlan.findFirst({
where: {
/* ... */
},
cacheStrategy: {
ttl: 60 * 60, // 1 hour TTL
swr: 60 * 60 * 4, // 4 hours SWR
tags: [
MacroTrackingCacheKeys.tags.userScheduled(userId),
MacroTrackingCacheKeys.tags.userScheduledDate(userId, date),
],
},
});
}
All cached data is validated using Zod schemas to ensure type safety:
// Define schema (source of truth)
export const foodLogEntrySchema = z.object({
id: z.number(),
foodId: z.number(),
// ... other fields
});
// Infer TypeScript type from schema
export type FoodLogEntry = z.infer<typeof foodLogEntrySchema>;
// Use in cachified for runtime validation
cachified({
checkValue: foodLogEntrySchema,
// ...
});
When editing a food log that changes dates, both the original and new dates are invalidated:
const datesToInvalidate = new Set<string>();
datesToInvalidate.add(existingLog.loggedDate);
if (updateData.loggedDate !== existingLog.loggedDate) {
datesToInvalidate.add(updateData.loggedDate);
}
When adding new cached endpoints:
macro-tracking-cache-keys.ts// Redis: Check Cachified logs
// Prisma: Monitor via Prisma Accelerate dashboard
// Pass forceFresh=true to Redis cached functions
getUserFoodLogsForDateCached(userId, date, true);
// Prisma: Manually invalidate tags
await db.$accelerate?.invalidate({
tags: [MacroTrackingCacheKeys.tags.user(userId)],
});
| Issue | Solution |
| ----------------------- | --------------------------------------------------------------- |
| Stale data after update | Check service function has invalidation logic |
| Cache stampede | Use forceFresh (background refresh) instead of cache deletion |
| Type mismatch errors | Ensure Zod schema matches database response |
| Missing invalidation | Check service functions have proper Redis invalidation |
_macro-tracking+/
├── macro-tracking-cache-keys.ts # Centralized cache keys
├── helpers/
│ └── macro-tracking-types.ts # Zod schemas & types
├── *.service.server.ts # Service functions with invalidation
├── *.db.server.ts # Database queries with caching
└── *.route.ts # Route handlers (no cache logic)
Promise.all