10LOC
Node.jsadvanced

A zero-dependency LRU cache built on Map's insertion order

Published June 4, 2027

export const createLRUCache = <K, V>(capacity: number) => {
  const cache = new Map<K, V>();

  const get = (key: K) => {
    if (!cache.has(key)) return undefined;
    const value = cache.get(key)!;
    cache.delete(key);
    cache.set(key, value);
    return value;
  };

  const set = (key: K, value: V) => {
    cache.delete(key);
    if (cache.size >= capacity) cache.delete(cache.keys().next().value as K);
    cache.set(key, value);
  };

  return { get, set };
};

What

createLRUCache(capacity) returns get/set functions backed by nothing but a Map. When the cache is full, set evicts the least-recently-used entry automatically.

Why it matters

The textbook LRU implementation is a hashmap plus a doubly-linked list, built to get O(1) eviction of an arbitrary "oldest" entry. JavaScript's Map already maintains insertion order as an iteration guarantee, and re-inserting a key moves it to the end of that order — which is exactly the "most recently used" position. That's the whole trick: get deletes and re-sets the key it just read, and set evicts by reading cache.keys().next().value, the single oldest key, before inserting the new one. No linked list, no extra bookkeeping struct, no dependency.

How it works

  • get returns early if the key isn't present. Otherwise it deletes the key and re-sets it with the same value — deleting and re-inserting is what moves it to the most-recently-used end of the Map's iteration order.
  • set always deletes the key first. If the key already existed, this drops the size by one before the capacity check, so overwriting an existing key never triggers an unnecessary eviction.
  • After that delete, if the cache is still at capacity, cache.keys().next().value reads the single oldest surviving key — the first one in iteration order — and evicts it.
  • The new key is inserted last, in both the "was already present" and "was evicted to make room" cases.

Gotchas

  • This took more than 11 lines, and stayed at that length on purpose (lineWaiver: true) — splitting get/set into fewer, denser statements would trade away the exact ordering that makes the eviction logic correct.
  • cache.get(key)! relies on the cache.has(key) check immediately above it. Don't refactor one without the other, or the assertion no longer holds.
  • There's no TTL here — eviction is purely capacity-based. Stash a timestamp in V if you also need entries to expire after a duration, independent of how often they're accessed.

Related

  • createTokenBucket — another closure-based, dependency-free algorithm; same "the standard library already has what you need" theme.
  • A real doubly-linked-list LRU is still worth reaching for if you need guaranteed O(1) eviction semantics independent of a JS engine's Map implementation — this version leans on V8's Map being fast in practice, not on a language guarantee.