Caching Patterns

Updated

September 8, 2026

Caching Patterns

Overview

Caches cut latency and load—and create consistency bugs. Start with explicit TTLs, single-flight fills, and clear invalidation.

In-process TTL map

type entry struct {
    val any
    exp time.Time
}

type Cache struct {
    mu sync.Mutex
    m  map[string]entry
}

func (c *Cache) Get(k string) (any, bool) {
    c.mu.Lock()
    defer c.mu.Unlock()
    e, ok := c.m[k]
    if !ok || time.Now().After(e.exp) {
        delete(c.m, k)
        return nil, false
    }
    return e.val, true
}

func (c *Cache) Set(k string, v any, ttl time.Duration) {
    c.mu.Lock()
    c.m[k] = entry{val: v, exp: time.Now().Add(ttl)}
    c.mu.Unlock()
}

Single-flight (stampede control)

go get golang.org/x/sync/singleflight@latest
var g singleflight.Group

v, err, _ := g.Do(key, func() (any, error) {
    return loadFromDB(ctx, key)
})

Cache-aside

get cache → miss → load DB → set cache → return

Write path: update DB then delete cache key (or short TTL).

Negative caching

Cache “not found” briefly to protect DB from repeated misses—short TTL.

Rules of thumb

Do Don’t
TTL + max entries Unbounded map growth
Invalidate on write Assume cache == truth forever
singleflight on hot keys Thundering herd on expiry

Try next

  1. Wrap Book repository with TTL cache.
  2. Benchmark stampede with/without singleflight.
  3. Add max keys with simple LRU eviction sketch.