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
getreturns 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.setalways 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().valuereads 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) — splittingget/setinto fewer, denser statements would trade away the exact ordering that makes the eviction logic correct. cache.get(key)!relies on thecache.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
Vif 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.