Go

Design a concurrency-safe, generic LRU cache with TTL.

Question 469HardGo 1.22 to 1.25

Classic O(1) LRU: a hash map from key to a node in a doubly linked list (container/list); move to front on access, evict from back when over capacity. TTL is checked lazily on Get. A single sync.Mutex is required (not RWMutex) because Get mutates recency.

type entry[K comparable, V any] struct {
	key     K
	val     V
	expires time.Time
}

type Cache[K comparable, V any] struct {
	mu    sync.Mutex
	cap   int
	ttl   time.Duration
	ll    *list.List
	items map[K]*list.Element
}

func New[K comparable, V any](capacity int, ttl time.Duration) *Cache[K, V] {
	return &Cache[K, V]{cap: capacity, ttl: ttl, ll: list.New(), items: make(map[K]*list.Element)}
}

func (c *Cache[K, V]) Get(k K) (V, bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	var zero V
	el, ok := c.items[k]
	if !ok {
		return zero, false
	}
	e := el.Value.(*entry[K, V])
	if time.Now().After(e.expires) {
		c.ll.Remove(el)
		delete(c.items, k)
		return zero, false
	}
	c.ll.MoveToFront(el)
	return e.val, true
}

func (c *Cache[K, V]) Set(k K, v V) {
	c.mu.Lock()
	defer c.mu.Unlock()
	exp := time.Now().Add(c.ttl)
	if el, ok := c.items[k]; ok {
		e := el.Value.(*entry[K, V])
		e.val, e.expires = v, exp
		c.ll.MoveToFront(el)
		return
	}
	c.items[k] = c.ll.PushFront(&entry[K, V]{key: k, val: v, expires: exp})
	if c.ll.Len() > c.cap {
		oldest := c.ll.Back()
		c.ll.Remove(oldest)
		delete(c.items, oldest.Value.(*entry[K, V]).key)
	}
}

Follow-ups: the entry stores its key so eviction can delete from the map; shard into N caches to reduce lock contention; add a janitor goroutine for expired-but-unread entries; prevent cache stampede with singleflight (next question); returned values are shared — callers must not mutate pointer/slice values.

More on Standard Library, HTTP & Systems Design in Go

All 35 Standard Library, HTTP & Systems Design in Go questions