Golang / GoLang System Architecture and Testing Interview Questions
What caching strategies do you use in Go microservices and how do you prevent cache stampede?
Caching reduces database load and improves latency. Common strategies in Go: in-memory (sync.Map, ristretto), distributed (Redis), and multi-level (L1 in-memory + L2 Redis). Cache stampede (thundering herd) is a classic distributed systems problem where many requests simultaneously miss a cold cache.
// Singleflight: collapse concurrent identical requests into one import "golang.org/x/sync/singleflight" type UserCache struct { mu sync.RWMutex local map[int64]*cachedUser redis *redis.Client repo UserRepository sf singleflight.Group } func (c *UserCache) Get(ctx context.Context, id int64) (*User, error) { // L1: in-memory cache (no network) c.mu.RLock() if cu, ok := c.local[id]; ok && time.Now().Before(cu.expires) { c.mu.RUnlock() return cu.user, nil } c.mu.RUnlock() // Singleflight: if 100 goroutines miss at the same time, // only ONE goes to Redis/DB â the other 99 wait for the result key := fmt.Sprintf("user:%d", id) result, err, _ := c.sf.Do(key, func() (any, error) { // L2: Redis cache data, err := c.redis.Get(ctx, key).Bytes() if err == nil { var u User json.Unmarshal(data, &u) c.storeLocal(id, &u) return &u, nil } // L3: database user, err := c.repo.FindByID(ctx, int(id)) if err != nil { return nil, err } // Populate caches (jitter TTL to avoid simultaneous expiry) jitter := time.Duration(rand.Intn(30)) * time.Second ttl := 5*time.Minute + jitter data, _ = json.Marshal(user) c.redis.Set(ctx, key, data, ttl) c.storeLocal(id, user) return user, nil }) if err != nil { return nil, err } return result.(*User), nil }
Cache stampede prevention: TTL jitter prevents all keys from expiring simultaneously (avoiding a mass DB hit). Singleflight collapses concurrent requests for the same key. Probabilistic early rehydration (XFetch algorithm) proactively refreshes cache before expiry based on computation time vs remaining TTL.
More Related questions...