DevPrep
  • Interview Prep
  • Projects
  • Resources
  • Pricing
  • About Us
Submit Question
DevPrep
  • Pricing
  • About Us
Submit Question

Practice

  • JavaScript
  • DSA
  • Machine Coding
  • System Design

Resources

  • Learning Tracks
  • Articles
  • Roadmaps
  • Compare Concepts
  • Glossary
  • Developer Tools
  • All Questions

Company

  • About
  • Pricing

Legal

  • Privacy Policy
  • Terms of Service
DevPrep

© 2026 DevPrep. All rights reserved.

← Back to Questions
HardMachine Coding

Implement a Thread-Safe LRU Cache

114 views

Problem Statement

Design and implement a thread-safe LRU (Least Recently Used) cache with O(1) Get and Put operations. This is a classic system design question asked at Google, Meta, and Amazon.

Requirements

  • Get(key): Return value if exists, mark as recently used
  • Put(key, value): Insert/update, evict LRU if at capacity
  • Both operations must be O(1)
  • Thread-safe for concurrent access

Data Structure


┌─────────────────────────────────────────────────────────┐
│                      LRU Cache                          │
├─────────────────────────────────────────────────────────┤
│  HashMap: key -> *Node (O(1) lookup)                   │
│                                                         │
│  Doubly Linked List: (O(1) insert/remove)              │
│                                                         │
│  HEAD ←→ [MRU] ←→ [Node] ←→ [Node] ←→ [LRU] ←→ TAIL   │
│   ↑                                              ↑      │
│   Most Recently Used              Least Recently Used   │
└─────────────────────────────────────────────────────────┘

Implementation

package lru

import (
    "container/list"
    "sync"
)

type entry struct {
    key   string
    value interface{}
}

type LRUCache struct {
    capacity int
    cache    map[string]*list.Element
    list     *list.List
    mu       sync.RWMutex
}

func NewLRUCache(capacity int) *LRUCache {
    return &LRUCache{
        capacity: capacity,
        cache:    make(map[string]*list.Element),
        list:     list.New(),
    }
}

func (c *LRUCache) Get(key string) (interface{}, bool) {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    if elem, ok := c.cache[key]; ok {
        c.list.MoveToFront(elem)
        return elem.Value.(*entry).value, true
    }
    return nil, false
}

func (c *LRUCache) Put(key string, value interface{}) {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    // Update existing
    if elem, ok := c.cache[key]; ok {
        c.list.MoveToFront(elem)
        elem.Value.(*entry).value = value
        return
    }
    
    // Evict if at capacity
    if c.list.Len() >= c.capacity {
        oldest := c.list.Back()
        if oldest != nil {
            c.list.Remove(oldest)
            delete(c.cache, oldest.Value.(*entry).key)
        }
    }
    
    // Insert new
    elem := c.list.PushFront(&entry{key: key, value: value})
    c.cache[key] = elem
}

func (c *LRUCache) Len() int {
    c.mu.RLock()
    defer c.mu.RUnlock()
    return c.list.Len()
}

func (c *LRUCache) Delete(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    if elem, ok := c.cache[key]; ok {
        c.list.Remove(elem)
        delete(c.cache, key)
    }
}

Usage Example

func main() {
    cache := lru.NewLRUCache(3)
    
    cache.Put("a", 1)
    cache.Put("b", 2)
    cache.Put("c", 3)
    
    cache.Get("a") // Access "a", moves to front
    
    cache.Put("d", 4) // Evicts "b" (least recently used)
    
    _, ok := cache.Get("b")
    fmt.Println("b exists:", ok) // false
}

Performance Optimization: Sharded LRU

type ShardedLRU struct {
    shards    []*LRUCache
    numShards int
}

func NewShardedLRU(numShards, capacityPerShard int) *ShardedLRU {
    shards := make([]*LRUCache, numShards)
    for i := range shards {
        shards[i] = NewLRUCache(capacityPerShard)
    }
    return &ShardedLRU{shards: shards, numShards: numShards}
}

func (s *ShardedLRU) getShard(key string) *LRUCache {
    hash := fnv.New32a()
    hash.Write([]byte(key))
    return s.shards[hash.Sum32()%uint32(s.numShards)]
}

func (s *ShardedLRU) Get(key string) (interface{}, bool) {
    return s.getShard(key).Get(key)
}

Follow-up Questions

  1. How would you implement TTL (time-to-live) for entries?
  2. How do you handle cache stampede (thundering herd)?
  3. What's the difference between LRU and LFU?

Sample Test Cases

Case 1
Input
capacity=3, ops=[put(1,a), put(2,b), put(3,c), get(1)]
Expected Output
Returns "a", key 1 moved to most recent
Case 2
Input
capacity=2, ops=[put(1,a), put(2,b), put(3,c)]
Expected Output
Key 1 evicted (LRU), keys 2,3 remain
Case 3
Input
{"capacity":3,"operations":[{"type":"put","key":"a","value":1},{"type":"put","key":"b","value":2},{"type":"put","key":"c","value":3},{"type":"get","key":"a"},{"type":"put","key":"d","value":4},{"type":"get","key":"b"}]}
Expected Output
[null,null,null,1,null,null]
Case 4
Input
{"capacity":2,"operations":[{"type":"put","key":"1","value":1},{"type":"put","key":"2","value":2},{"type":"get","key":"1"},{"type":"put","key":"3","value":3},{"type":"get","key":"2"},{"type":"put","key":"4","value":4},{"type":"get","key":"1"},{"type":"get","key":"3"},{"type":"get","key":"4"}]}
Expected Output
[null,null,1,null,null,null,null,3,4]
Case 5
Input
{"capacity":1,"operations":[{"type":"put","key":"x","value":10},{"type":"get","key":"x"},{"type":"put","key":"y","value":20},{"type":"get","key":"x"},{"type":"get","key":"y"}]}
Expected Output
[null,10,null,null,20]

No solutions yet

Be the first to share a solution for this question.

Comments (0)

Sign in to leave a comment.

No comments yet. Be the first to comment.

Stats

Views
114
Likes
0
Solutions
0
Comments
0

Category

Backend Engineering

Languages

Go