Macro Tracking Caching Strategy

Overview

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.

Architecture

Two-Tier Caching System

  1. Redis Cache (via Cachified) - For frequently changing data

  2. Prisma Accelerate - For stable data

Cache Keys Management

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
  },
};

Complete Cache Invalidation Matrix

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} |

Cache Invalidation Strategy

Key Principles

  1. Service Layer Invalidation: All cache invalidation happens in service files (*.service.server.ts), not in route handlers
  2. forceFresh for Redis: Uses forceFresh: true to trigger background refresh without blocking
  3. Tag-Based Invalidation: Prisma uses tags for targeted cache clearing

Invalidation Matrix

| 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 |

Implementation Patterns

1. Database Query with Caching

// 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);
    },
  });
}

2. Service Function with Invalidation

// 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);
}

3. Prisma Query with Cache Strategy

// 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),
      ],
    },
  });
}

Type Safety with Zod

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,
  // ...
});

Cost Management

Prisma Accelerate Limits (Business Plan)

Redis Cache

Special Considerations

Date Changes

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);
}

Adding New Cached Endpoints

When adding new cached endpoints:

  1. Determine cache tier: High-change frequency → Redis, Stable → Prisma
  2. Add cache keys: Update macro-tracking-cache-keys.ts
  3. Implement caching: Follow patterns above
  4. Add invalidation: Update relevant service functions
  5. Document: Update this file and invalidation matrix

Monitoring and Debugging

Check Cache Hit Rates

// Redis: Check Cachified logs
// Prisma: Monitor via Prisma Accelerate dashboard

Force Cache Refresh

// Pass forceFresh=true to Redis cached functions
getUserFoodLogsForDateCached(userId, date, true);

// Prisma: Manually invalidate tags
await db.$accelerate?.invalidate({
  tags: [MacroTrackingCacheKeys.tags.user(userId)],
});

Common Issues and Solutions

| 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 |

File Structure

_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)

Best Practices

  1. Never put cache invalidation in route handlers - Always use service functions
  2. Use date-specific cache keys when data is date-bound
  3. Validate cached data with Zod schemas
  4. Batch invalidations - Execute in parallel with Promise.all
  5. Keep it simple - Only invalidate what actually changes
  6. Document special cases in code comments