Caching Patterns
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@latestvar 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
- Wrap Book repository with TTL cache.
- Benchmark stampede with/without singleflight.
- Add max keys with simple LRU eviction sketch.