We are building an in-memory session cache for an API gateway handling 50k requests/sec. We need an LRU (Least Recently Used) cache where both get() and put() operations happen strictly in O(1) time.
A lot of implementations I see online either use standard library ordered dicts or clumsy doubly linked lists that trigger dangling pointers and memory leaks when nodes get evicted. What is the standard architectural pattern used by experienced engineers to build a bulletproof LRU cache?
In C++, a common mistake when building an LRU cache is using
std::listwhich allocates each node on the heap separately. In production, we use a custom intrusive doubly linked list with a pool or flat hash map (std::unordered_map) to guaranteeO(1)latency with minimal heap fragmentation.Memory Safety:
std::unique_ptrowns the node memory, preventing any memory leaks even if exceptions occur.Building an LRU cache from scratch is one of the best ways to understand how data structures combine in real systems. The industry standard pattern is combining two complementary structures:
O(1)key-to-node lookups.O(1)node insertion at the front (most recent) andO(1)node removal from the back (least recent).The secret trick that eliminates 90% of bug-prone null checks is using sentinel (dummy) head and tail nodes. Instead of constantly checking
if (head == null)orif (node->prev == null), the dummy head and tail are always linked together:head <-> tail. Any real data node always lives safely in between them!Architectural Diagram
Clean, Idiomatic Python 3.12 Implementation
Why Storing the Key Inside the Node is Crucial
Notice that the
Nodeclass stores bothkeyandval. Many developers forget to storekeyin the node and only storeval. But when the cache reaches full capacity and you evicttail.prev, how do you delete that entry from the hash map? Withoutnode.key, you’d have to search the entire hash map inO(N)time, destroying yourO(1)guarantee!Thread Safety in Production
If multiple threads access this cache concurrently:
threading.Lock()aroundgetandput.sync.RWMutex/std::shared_mutex) is often tempting, but remember: even aget()operation mutates the linked list (to move the accessed item to the front)! Therefore, standard read locks are not enough—you must acquire an exclusive lock or use lock striping across multiple shards.