// File Path: monorepo/cloud/maplepress-backend/pkg/cache/twotier.go package cache import ( "context" "time" "go.uber.org/zap" ) // TwoTierCacher defines the interface for two-tier cache operations type TwoTierCacher interface { Shutdown(ctx context.Context) Get(ctx context.Context, key string) ([]byte, error) Set(ctx context.Context, key string, val []byte) error SetWithExpiry(ctx context.Context, key string, val []byte, expiry time.Duration) error Delete(ctx context.Context, key string) error PurgeExpired(ctx context.Context) error } // twoTierCache implements a clean 2-layer (read-through write-through) cache // // L1: Redis (fast, in-memory) // L2: Cassandra (persistent) // // On Get: check Redis → then Cassandra → if found in Cassandra → populate Redis // On Set: write to both // On SetWithExpiry: write to both with expiry // On Delete: remove from both type twoTierCache struct { redisCache RedisCacher cassandraCache CassandraCacher logger *zap.Logger } // NewTwoTierCache creates a new two-tier cache instance func NewTwoTierCache(redisCache RedisCacher, cassandraCache CassandraCacher, logger *zap.Logger) TwoTierCacher { logger = logger.Named("two-tier-cache") logger.Info("✓ Two-tier cache initialized (Redis L1 + Cassandra L2)") return &twoTierCache{ redisCache: redisCache, cassandraCache: cassandraCache, logger: logger, } } func (c *twoTierCache) Get(ctx context.Context, key string) ([]byte, error) { // Try L1 (Redis) first val, err := c.redisCache.Get(ctx, key) if err != nil { return nil, err } if val != nil { c.logger.Debug("cache hit from Redis", zap.String("key", key)) return val, nil } // Not in Redis, try L2 (Cassandra) val, err = c.cassandraCache.Get(ctx, key) if err != nil { return nil, err } if val != nil { // Found in Cassandra, populate Redis for future lookups c.logger.Debug("cache hit from Cassandra, writing back to Redis", zap.String("key", key)) _ = c.redisCache.Set(ctx, key, val) // Best effort, don't fail if Redis write fails } return val, nil } func (c *twoTierCache) Set(ctx context.Context, key string, val []byte) error { // Write to both layers if err := c.redisCache.Set(ctx, key, val); err != nil { return err } if err := c.cassandraCache.Set(ctx, key, val); err != nil { return err } return nil } func (c *twoTierCache) SetWithExpiry(ctx context.Context, key string, val []byte, expiry time.Duration) error { // Write to both layers with expiry if err := c.redisCache.SetWithExpiry(ctx, key, val, expiry); err != nil { return err } if err := c.cassandraCache.SetWithExpiry(ctx, key, val, expiry); err != nil { return err } return nil } func (c *twoTierCache) Delete(ctx context.Context, key string) error { // Remove from both layers if err := c.redisCache.Delete(ctx, key); err != nil { return err } if err := c.cassandraCache.Delete(ctx, key); err != nil { return err } return nil } func (c *twoTierCache) PurgeExpired(ctx context.Context) error { // Only Cassandra needs purging (Redis handles TTL automatically) return c.cassandraCache.PurgeExpired(ctx) } func (c *twoTierCache) Shutdown(ctx context.Context) { c.logger.Info("shutting down two-tier cache") c.redisCache.Shutdown(ctx) c.cassandraCache.Shutdown(ctx) c.logger.Info("two-tier cache shutdown complete") }